From 13a1ee3da9dd8f2be6f7d932875169c1a4ce9562 Mon Sep 17 00:00:00 2001 From: shinhong_park Date: Tue, 22 Oct 2024 23:55:18 +0900 Subject: [PATCH] Rewrite SyllabusWebView (#318) * Rewrite SyllabusWebView * Apply SwiftFormat changes * Fix as reviews --------- Co-authored-by: shp7724 --- SNUTT-2022/SNUTT.xcodeproj/project.pbxproj | 46 ++--- .../Components/WebViews/SyllabusWebView.swift | 19 -- .../SyllabusFilePreviewController.swift | 31 +++ .../SyllabusWebView/SyllabusWebView.swift | 190 ++++++++++++++++++ .../WebBottomNavigationView.swift | 115 +++++++++++ .../SyllabusWebView/WebProgressView.swift | 60 ++++++ .../Views/Scenes/LectureDetailScene.swift | 7 +- 7 files changed, 421 insertions(+), 47 deletions(-) delete mode 100644 SNUTT-2022/SNUTT/Views/Components/WebViews/SyllabusWebView.swift create mode 100644 SNUTT-2022/SNUTT/Views/Components/WebViews/SyllabusWebView/SyllabusFilePreviewController.swift create mode 100644 SNUTT-2022/SNUTT/Views/Components/WebViews/SyllabusWebView/SyllabusWebView.swift create mode 100644 SNUTT-2022/SNUTT/Views/Components/WebViews/SyllabusWebView/WebBottomNavigationView.swift create mode 100644 SNUTT-2022/SNUTT/Views/Components/WebViews/SyllabusWebView/WebProgressView.swift diff --git a/SNUTT-2022/SNUTT.xcodeproj/project.pbxproj b/SNUTT-2022/SNUTT.xcodeproj/project.pbxproj index ea21bcc81..7bd11b689 100644 --- a/SNUTT-2022/SNUTT.xcodeproj/project.pbxproj +++ b/SNUTT-2022/SNUTT.xcodeproj/project.pbxproj @@ -282,6 +282,9 @@ CEB5A71E29DD9CD400E74C47 /* TimetableAccessoryRectangularView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEB5A71D29DD9CD400E74C47 /* TimetableAccessoryRectangularView.swift */; }; CEB5A72029DDA8B200E74C47 /* TimetableAccessoryInlineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEB5A71F29DDA8B200E74C47 /* TimetableAccessoryInlineView.swift */; }; CEC91CAA2A812499008B0F15 /* VacancyEmptyListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEC91CA92A812499008B0F15 /* VacancyEmptyListView.swift */; }; + CECDD36C2CC3E45000BB5E5B /* SyllabusFilePreviewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CECDD36B2CC3E44F00BB5E5B /* SyllabusFilePreviewController.swift */; }; + CECDD36E2CC3E47300BB5E5B /* WebBottomNavigationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CECDD36D2CC3E47000BB5E5B /* WebBottomNavigationView.swift */; }; + CECDD3702CC3E4B400BB5E5B /* WebProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CECDD36F2CC3E4B000BB5E5B /* WebProgressView.swift */; }; CECE96642A72D5F2003D87FA /* VacancyBanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = CECE96632A72D5F2003D87FA /* VacancyBanner.swift */; }; CEDDCA7B2A6AE62B00474D4E /* VacancyLectureCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEDDCA7A2A6AE62B00474D4E /* VacancyLectureCell.swift */; }; CEDDCA7E2A6AEF2200474D4E /* VacancyLectureList.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEDDCA7D2A6AEF2200474D4E /* VacancyLectureList.swift */; }; @@ -591,6 +594,9 @@ CEB5A71D29DD9CD400E74C47 /* TimetableAccessoryRectangularView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimetableAccessoryRectangularView.swift; sourceTree = ""; }; CEB5A71F29DDA8B200E74C47 /* TimetableAccessoryInlineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimetableAccessoryInlineView.swift; sourceTree = ""; }; CEC91CA92A812499008B0F15 /* VacancyEmptyListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VacancyEmptyListView.swift; sourceTree = ""; }; + CECDD36B2CC3E44F00BB5E5B /* SyllabusFilePreviewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyllabusFilePreviewController.swift; sourceTree = ""; }; + CECDD36D2CC3E47000BB5E5B /* WebBottomNavigationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebBottomNavigationView.swift; sourceTree = ""; }; + CECDD36F2CC3E4B000BB5E5B /* WebProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebProgressView.swift; sourceTree = ""; }; CECE96632A72D5F2003D87FA /* VacancyBanner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VacancyBanner.swift; sourceTree = ""; }; CEDDCA7A2A6AE62B00474D4E /* VacancyLectureCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VacancyLectureCell.swift; sourceTree = ""; }; CEDDCA7D2A6AEF2200474D4E /* VacancyLectureList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VacancyLectureList.swift; sourceTree = ""; }; @@ -725,7 +731,7 @@ BE28036328E8850E00B2B1AB /* WebViewType.swift */, BE28036128E884F400B2B1AB /* ReviewWebView.swift */, BE28036528E8854D00B2B1AB /* SingleWebView.swift */, - B89826D028F4971000477A14 /* SyllabusWebView.swift */, + CECDD36A2CC3E43400BB5E5B /* SyllabusWebView */, B8F40EAE28980D990021A2A9 /* DeveloperInfoView.swift */, B8F40EB028980DE00021A2A9 /* TermsOfServiceView.swift */, B8F40EB228980DFF0021A2A9 /* PrivacyPolicyView.swift */, @@ -876,6 +882,17 @@ path = Lecture; sourceTree = ""; }; + CECDD36A2CC3E43400BB5E5B /* SyllabusWebView */ = { + isa = PBXGroup; + children = ( + B89826D028F4971000477A14 /* SyllabusWebView.swift */, + CECDD36B2CC3E44F00BB5E5B /* SyllabusFilePreviewController.swift */, + CECDD36D2CC3E47000BB5E5B /* WebBottomNavigationView.swift */, + CECDD36F2CC3E4B000BB5E5B /* WebProgressView.swift */, + ); + path = SyllabusWebView; + sourceTree = ""; + }; CEDDCA7C2A6AEF0E00474D4E /* Vacancy */ = { isa = PBXGroup; children = ( @@ -1227,7 +1244,6 @@ DC860F4227E5C87D0068C94B /* Frameworks */, DC860F4327E5C87D0068C94B /* Resources */, BE98A073288AFC1600C2CE95 /* Embed Foundation Extensions */, - BEBB3C14292CCD960023F695 /* Upload dSym to Firebase */, ); buildRules = ( ); @@ -1388,29 +1404,6 @@ }; /* End PBXResourcesBuildPhase section */ -/* Begin PBXShellScriptBuildPhase section */ - BEBB3C14292CCD960023F695 /* Upload dSym to Firebase */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - "${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}/Contents/Resources/DWARF/${TARGET_NAME}", - "$(SRCROOT)/$(BUILT_PRODUCTS_DIR)/$(INFOPLIST_PATH)", - ); - name = "Upload dSym to Firebase"; - outputFileListPaths = ( - ); - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\nif [ \"${CONFIGURATION}\" = \"Release\" ]; \nthen \n${BUILD_DIR%/Build/*}/SourcePackages/checkouts/firebase-ios-sdk/Crashlytics/run -gsp \"${PROJECT_DIR}\"/SNUTT/GoogleServiceReleaseInfo.plist\n${BUILD_DIR%/Build/*}/SourcePackages/checkouts/firebase-ios-sdk/Crashlytics/upload-symbols -gsp \"${PROJECT_DIR}\"/SNUTT/GoogleServiceReleaseInfo.plist -p ios ${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}\nelse\n${BUILD_DIR%/Build/*}/SourcePackages/checkouts/firebase-ios-sdk/Crashlytics/run -gsp \"${PROJECT_DIR}\"/SNUTT/GoogleServiceDebugInfo.plist\n${BUILD_DIR%/Build/*}/SourcePackages/checkouts/firebase-ios-sdk/Crashlytics/upload-symbols -gsp \"${PROJECT_DIR}\"/SNUTT/GoogleServiceDebugInfo.plist -p ios ${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}\nfi\n"; - }; -/* End PBXShellScriptBuildPhase section */ - /* Begin PBXSourcesBuildPhase section */ BE98A05D288AFC1500C2CE95 /* Sources */ = { isa = PBXSourcesBuildPhase; @@ -1505,6 +1498,7 @@ B87DF6F129150A00008BB95B /* PopupService.swift in Sources */, DC29159D2865FA2800FE5F9A /* SettingViewModel.swift in Sources */, DC1E0ED12877381F005632A3 /* Router.swift in Sources */, + CECDD36E2CC3E47300BB5E5B /* WebBottomNavigationView.swift in Sources */, B87B315D28D5A70F005C170B /* UserState.swift in Sources */, B87DF6F92918B7AD008BB95B /* PopupState.swift in Sources */, B87B316D28D755B6005C170B /* MenuState.swift in Sources */, @@ -1525,6 +1519,7 @@ DC1E0ECF28772F13005632A3 /* NetworkUtils.swift in Sources */, BE6D893528EFD68E000607A6 /* String+Localized.swift in Sources */, BEB3B6A228CDB7A400E56062 /* LectureTimePicker.swift in Sources */, + CECDD3702CC3E4B400BB5E5B /* WebProgressView.swift in Sources */, BE77D0AC28B1384D0067A9D8 /* MenuSheetTopBar.swift in Sources */, BEDF506F27EB744A00CDCC13 /* LectureDetailScene.swift in Sources */, BE98A06F288AFC1600C2CE95 /* SNUTTWidget.intentdefinition in Sources */, @@ -1627,6 +1622,7 @@ BE9413D928C3AFA500171060 /* EmptySearchResult.swift in Sources */, CE72435F2B30235300F9E0D7 /* SearchLectureSceneViewModel.swift in Sources */, CEF4200F2A62ADE3005C2B1F /* FriendsViewModel.swift in Sources */, + CECDD36C2CC3E45000BB5E5B /* SyllabusFilePreviewController.swift in Sources */, B88D16F328ABC5DD00E2D652 /* String+Ext.swift in Sources */, BEDE34D62879A7B800525014 /* DIContainer.swift in Sources */, BE9413D228C2458A00171060 /* DateFormatter+Parse.swift in Sources */, diff --git a/SNUTT-2022/SNUTT/Views/Components/WebViews/SyllabusWebView.swift b/SNUTT-2022/SNUTT/Views/Components/WebViews/SyllabusWebView.swift deleted file mode 100644 index 05a47947b..000000000 --- a/SNUTT-2022/SNUTT/Views/Components/WebViews/SyllabusWebView.swift +++ /dev/null @@ -1,19 +0,0 @@ -// -// SyllabusWebView.swift -// SNUTT -// -// Created by 최유림 on 2022/10/11. -// - -import SafariServices -import SwiftUI - -struct SyllabusWebView: UIViewControllerRepresentable { - @Binding var url: String - - func makeUIViewController(context _: UIViewControllerRepresentableContext) -> SFSafariViewController { - return SFSafariViewController(url: URL(string: url)!) - } - - func updateUIViewController(_: SFSafariViewController, context _: UIViewControllerRepresentableContext) {} -} diff --git a/SNUTT-2022/SNUTT/Views/Components/WebViews/SyllabusWebView/SyllabusFilePreviewController.swift b/SNUTT-2022/SNUTT/Views/Components/WebViews/SyllabusWebView/SyllabusFilePreviewController.swift new file mode 100644 index 000000000..28536f152 --- /dev/null +++ b/SNUTT-2022/SNUTT/Views/Components/WebViews/SyllabusWebView/SyllabusFilePreviewController.swift @@ -0,0 +1,31 @@ +// +// SyllabusFilePreviewController.swift +// SNUTT +// +// Created by 박신홍 on 10/19/24. +// + +import QuickLook + +final class SyllabusFilePreviewController: QLPreviewController { + private let item: any QLPreviewItem + init(item: any QLPreviewItem) { + self.item = item + super.init(nibName: nil, bundle: nil) + dataSource = self + } + + @available(*, unavailable) required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +extension SyllabusFilePreviewController: QLPreviewControllerDataSource { + func numberOfPreviewItems(in _: QLPreviewController) -> Int { + 1 + } + + func previewController(_: QLPreviewController, previewItemAt _: Int) -> any QLPreviewItem { + item + } +} diff --git a/SNUTT-2022/SNUTT/Views/Components/WebViews/SyllabusWebView/SyllabusWebView.swift b/SNUTT-2022/SNUTT/Views/Components/WebViews/SyllabusWebView/SyllabusWebView.swift new file mode 100644 index 000000000..75e8e7e59 --- /dev/null +++ b/SNUTT-2022/SNUTT/Views/Components/WebViews/SyllabusWebView/SyllabusWebView.swift @@ -0,0 +1,190 @@ +// +// SyllabusWebView.swift +// SNUTT +// +// Created by 최유림 on 2022/10/11. +// + +import Combine +import QuickLook +import SwiftUI +import UIKit +import WebKit + +struct SyllabusWebView: UIViewControllerRepresentable { + let lectureTitle: String + @Binding var urlString: String + + func makeUIViewController(context _: UIViewControllerRepresentableContext) -> UIViewController { + let webViewController = SyllabusWebViewController(entryURL: URL(string: urlString)) + let navigationController = UINavigationController(rootViewController: webViewController) + let appearance = UINavigationBarAppearance() + appearance.configureWithDefaultBackground() + navigationController.navigationBar.standardAppearance = appearance + navigationController.navigationBar.scrollEdgeAppearance = appearance + webViewController.navigationItem.rightBarButtonItem = .init(systemItem: .close, primaryAction: .init(handler: { [weak navigationController] _ in + navigationController?.dismiss(animated: true) + })) + webViewController.title = lectureTitle + return navigationController + } + + func updateUIViewController(_: UIViewController, context _: UIViewControllerRepresentableContext) {} +} + +final class SyllabusWebViewController: UIViewController { + private enum Constants { + static let homeURL = URL(string: "https://sugang.snu.ac.kr")! + static let referer = "https://sugang.snu.ac.kr/sugang/cc/cc100InterfaceExcel.action" + } + + private enum Design { + static let bottomViewHeight = 50.0 + } + + private let entryURL: URL + private lazy var webView: WKWebView = { + let config = WKWebViewConfiguration() + let webView = WKWebView(frame: .zero, configuration: config) + webView.allowsBackForwardNavigationGestures = true + webView.scrollView.contentInset = .init(top: 0, left: 0, bottom: Design.bottomViewHeight, right: 0) + webView.navigationDelegate = self + return webView + }() + + private var cancellables = Set() + private let bottomView = WebBottomNavigationView() + private var downloadLocalURLs = [WKDownload: URL]() + + init(entryURL: URL?) { + self.entryURL = entryURL ?? Constants.homeURL + super.init(nibName: nil, bundle: nil) + } + + @available(*, unavailable) required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + setupView() + bindBottomButtons() + initialLoad() + } + + private func setupView() { + view.addSubview(webView) + view.addSubview(bottomView) + webView.translatesAutoresizingMaskIntoConstraints = false + bottomView.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + webView.topAnchor.constraint(equalTo: view.topAnchor), + webView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + webView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + webView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + bottomView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + bottomView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + bottomView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + bottomView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -Design.bottomViewHeight), + ]) + } + + private func initialLoad() { + var request = URLRequest(url: entryURL) + request.setValue(Constants.referer, forHTTPHeaderField: "Referer") + webView.load(request) + } + + private func bindBottomButtons() { + bottomView.buttonDidPressPublisher + .sink { [weak self] button in + guard let self else { return } + switch button { + case .back: + webView.goBack() + case .forward: + webView.goForward() + case .reload: + webView.reload() + case .safari: + UIApplication.shared.open(entryURL) + } + } + .store(in: &cancellables) + webView.publisher(for: \.canGoBack) + .sink { [weak self] canGoBack in + guard let self else { return } + bottomView.button(for: .back).isEnabled = canGoBack + } + .store(in: &cancellables) + webView.publisher(for: \.canGoForward) + .sink { [weak self] canGoForward in + guard let self else { return } + bottomView.button(for: .forward).isEnabled = canGoForward + } + .store(in: &cancellables) + webView.publisher(for: \.estimatedProgress) + .sink { [weak self] progress in + guard let self else { return } + bottomView.progressView.setProgress(progress) + print("[progress] \(progress)") + } + .store(in: &cancellables) + } +} + +extension SyllabusWebViewController: WKNavigationDelegate { + func webView(_: WKWebView, decidePolicyFor _: WKNavigationAction) async -> WKNavigationActionPolicy { + return .allow + } + + func webView(_: WKWebView, decidePolicyFor navigationResponse: WKNavigationResponse) async -> WKNavigationResponsePolicy { + guard let mimeType = navigationResponse.response.mimeType else { return .cancel } + if mimeType == "text/html" { + return .allow + } + return .download + } + + func webView(_: WKWebView, navigationResponse _: WKNavigationResponse, didBecome download: WKDownload) { + download.delegate = self + } +} + +extension SyllabusWebViewController: WKDownloadDelegate { + func download(_ download: WKDownload, decideDestinationUsing _: URLResponse, suggestedFilename: String) async -> URL? { + let percentDecoded = suggestedFilename.removingPercentEncoding ?? suggestedFilename + let fileManager = FileManager.default + let url = fileManager.temporaryDirectory.appendingPathComponent(percentDecoded) + try? fileManager.removeItem(at: url) + downloadLocalURLs[download] = url + return url + } + + func downloadDidFinish(_ download: WKDownload) { + guard let localURL = downloadLocalURLs.removeValue(forKey: download) else { return } + let previewController = SyllabusFilePreviewController(item: localURL as QLPreviewItem) + present(previewController, animated: true) + } +} + +extension UIControl { + func addAction( + for controlEvents: UIControl.Event = .touchUpInside, + _ closure: @MainActor @escaping () -> Void + ) { + addAction(UIAction { (_: UIAction) in closure() }, for: controlEvents) + } +} + +@available(iOS 17.0, *) +#Preview { + SyllabusWebView(lectureTitle: "수강스누", urlString: .constant("https://sugang.snu.ac.kr/sugang/cc/cc103.action?openSchyy=2024&openShtmFg=U000200002&openDetaShtmFg=U000300002&sbjtCd=M3500.002000<No=001&sbjtSubhCd=000")) + .ignoresSafeArea(edges: .bottom) +} + +@available(iOS 17.0, *) +#Preview { + SyllabusWebView(lectureTitle: "네이버", urlString: .constant("https://naver.com")) + .ignoresSafeArea(edges: .bottom) +} diff --git a/SNUTT-2022/SNUTT/Views/Components/WebViews/SyllabusWebView/WebBottomNavigationView.swift b/SNUTT-2022/SNUTT/Views/Components/WebViews/SyllabusWebView/WebBottomNavigationView.swift new file mode 100644 index 000000000..fa6e256e2 --- /dev/null +++ b/SNUTT-2022/SNUTT/Views/Components/WebViews/SyllabusWebView/WebBottomNavigationView.swift @@ -0,0 +1,115 @@ +// +// WebBottomNavigationView.swift +// SNUTT +// +// Created by 박신홍 on 10/19/24. +// + +import Combine +import UIKit + +final class WebBottomNavigationView: UIView { + private var buttons = [ButtonType: UIButton]() + func button(for type: ButtonType) -> UIButton { + buttons[type]! + } + + var buttonDidPressPublisher = PassthroughSubject() + + override init(frame: CGRect) { + super.init(frame: .zero) + setupView() + setupButtons() + } + + @available(*, unavailable) required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + let progressView = WebProgressView() + + private func setupView() { + addSubview(backgroundView) + addSubview(horizontalButtonView) + addSubview(progressView) + backgroundView.translatesAutoresizingMaskIntoConstraints = false + horizontalButtonView.translatesAutoresizingMaskIntoConstraints = false + progressView.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + backgroundView.leadingAnchor.constraint(equalTo: leadingAnchor), + backgroundView.trailingAnchor.constraint(equalTo: trailingAnchor), + backgroundView.topAnchor.constraint(equalTo: topAnchor), + backgroundView.bottomAnchor.constraint(equalTo: bottomAnchor), + horizontalButtonView.leadingAnchor.constraint(equalTo: leadingAnchor), + horizontalButtonView.trailingAnchor.constraint(equalTo: trailingAnchor), + horizontalButtonView.topAnchor.constraint(equalTo: topAnchor), + horizontalButtonView.bottomAnchor.constraint(equalTo: safeAreaLayoutGuide.bottomAnchor), + progressView.leadingAnchor.constraint(equalTo: leadingAnchor), + progressView.trailingAnchor.constraint(equalTo: trailingAnchor), + progressView.bottomAnchor.constraint(equalTo: backgroundView.topAnchor), + ]) + } + + private let backgroundView: UIView = { + let blurEffect = UIBlurEffect(style: .systemThinMaterial) + let blurView = UIVisualEffectView(effect: blurEffect) + let divider = UIView() + divider.backgroundColor = .label.withAlphaComponent(0.1) + divider.translatesAutoresizingMaskIntoConstraints = false + blurView.contentView.addSubview(divider) + NSLayoutConstraint.activate([ + divider.topAnchor.constraint(equalTo: blurView.topAnchor), + divider.leadingAnchor.constraint(equalTo: blurView.leadingAnchor), + divider.trailingAnchor.constraint(equalTo: blurView.trailingAnchor), + divider.heightAnchor.constraint(equalToConstant: 1), + ]) + return blurView + }() + + private let horizontalButtonView: UIStackView = { + let stackView = UIStackView() + stackView.axis = .horizontal + stackView.distribution = .fillEqually + return stackView + }() + + private func setupButtons() { + for buttonType in ButtonType.allCases { + let button = makeButton(for: buttonType) + buttons[buttonType] = button + button.addAction { [weak self] in + self?.buttonDidPressPublisher.send(buttonType) + } + horizontalButtonView.addArrangedSubview(button) + } + } + + private func makeButton(for type: ButtonType) -> UIButton { + var config = UIButton.Configuration.plain() + config.image = type.image + let button = UIButton(configuration: config) + return button + } +} + +extension WebBottomNavigationView { + enum ButtonType: CaseIterable { + case back + case forward + case reload + case safari + + var image: UIImage? { + switch self { + case .back: + UIImage(systemName: "chevron.backward") + case .forward: + UIImage(systemName: "chevron.forward") + case .reload: + UIImage(systemName: "arrow.clockwise") + case .safari: + UIImage(systemName: "safari") + } + } + } +} diff --git a/SNUTT-2022/SNUTT/Views/Components/WebViews/SyllabusWebView/WebProgressView.swift b/SNUTT-2022/SNUTT/Views/Components/WebViews/SyllabusWebView/WebProgressView.swift new file mode 100644 index 000000000..d5309c730 --- /dev/null +++ b/SNUTT-2022/SNUTT/Views/Components/WebViews/SyllabusWebView/WebProgressView.swift @@ -0,0 +1,60 @@ +// +// WebProgressView.swift +// SNUTT +// +// Created by 박신홍 on 10/19/24. +// + +import UIKit + +final class WebProgressView: UIView { + private let progressBar = UIView() + private var progress: Double = 0 { + didSet { + updateProgressBarFrame() + } + } + + override init(frame: CGRect) { + super.init(frame: frame) + setupView() + } + + @available(*, unavailable) required init?(coder _: NSCoder) { + fatalError() + } + + private func setupView() { + progressBar.backgroundColor = .systemBlue.withAlphaComponent(0.9) + progressBar.layer.shadowColor = UIColor.blue.withAlphaComponent(0.5).cgColor + progressBar.layer.masksToBounds = false + addSubview(progressBar) + setProgress(0) + } + + func setProgress(_ value: Double) { + let newProgress = min(max(value, 0.0), 1.0) + if newProgress < progress { + progress = 0 + } + UIView.animate(withDuration: 0.3, delay: 0, options: [.beginFromCurrentState]) { + self.progress = newProgress + } + if progress == 1 { + UIView.animate(withDuration: 0.3, delay: 0.5) { + self.progressBar.alpha = 0 + } + } else { + progressBar.alpha = 1 + } + } + + private func updateProgressBarFrame() { + let progressBarWidth = CGFloat(progress) * bounds.width + progressBar.frame = CGRect(x: 0, y: 0, width: progressBarWidth, height: bounds.height) + } + + override var intrinsicContentSize: CGSize { + .init(width: UIView.noIntrinsicMetric, height: 2) + } +} diff --git a/SNUTT-2022/SNUTT/Views/Scenes/LectureDetailScene.swift b/SNUTT-2022/SNUTT/Views/Scenes/LectureDetailScene.swift index e401f8a80..16ab5be78 100644 --- a/SNUTT-2022/SNUTT/Views/Scenes/LectureDetailScene.swift +++ b/SNUTT-2022/SNUTT/Views/Scenes/LectureDetailScene.swift @@ -493,9 +493,10 @@ struct LectureDetailScene: View { showSyllabusWebView = true } } - }.fullScreenCover(isPresented: $showSyllabusWebView) { - SyllabusWebView(url: $syllabusURL) - .edgesIgnoringSafeArea(.all) + }.sheet(isPresented: $showSyllabusWebView) { + SyllabusWebView(lectureTitle: lecture.title, urlString: $syllabusURL) + .ignoresSafeArea(edges: .bottom) + .interactiveDismissDisabled() } DetailButton(text: "강의평") {