diff --git a/iBox/Sources/AppDelegate.swift b/iBox/Sources/AppDelegate.swift index 8ba7e6f..eb8fbd4 100644 --- a/iBox/Sources/AppDelegate.swift +++ b/iBox/Sources/AppDelegate.swift @@ -21,7 +21,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { } versioningHandler.checkAppVersion { result in - AppStateManager.shared.isVersionCheckCompleted = result + AppStateManager.shared.versionCheckCompleted = result } return true diff --git a/iBox/Sources/BoxList/BoxListView.swift b/iBox/Sources/BoxList/BoxListView.swift index 63e6d40..abbaba0 100644 --- a/iBox/Sources/BoxList/BoxListView.swift +++ b/iBox/Sources/BoxList/BoxListView.swift @@ -378,31 +378,40 @@ extension BoxListView: UITableViewDelegate { func tableView(_ tableView: UITableView, contextMenuConfigurationForRowAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? { let configuration = UIContextMenuConfiguration(identifier: indexPath as NSIndexPath, previewProvider: { [weak self] () -> UIViewController? in - guard let self = self, let cellViewModel = self.viewModel?.boxList[indexPath.section].boxListCellViewModelsWithStatus[indexPath.row] else { return nil } - - let id = cellViewModel.id - let url = cellViewModel.url - let name = cellViewModel.name - - let previewViewController: UIViewController - - if let cachedViewController = WebCacheManager.shared.viewControllerForKey(id) { - previewViewController = cachedViewController - } else { - let viewController = WebViewController() - viewController.selectedWebsite = url - viewController.title = name - WebCacheManager.shared.cacheData(forKey: id, viewController: viewController) - previewViewController = viewController - } - - return previewViewController + guard let self = self else { return nil } + return self.createOrRetrievePreviewController(for: indexPath) }, actionProvider: { suggestedActions in return self.makeContextMenu(for: indexPath) }) return configuration } + private func createOrRetrievePreviewController(for indexPath: IndexPath) -> UIViewController? { + guard let cellViewModel = self.viewModel?.boxList[indexPath.section].boxListCellViewModelsWithStatus[indexPath.row] else { return nil } + let id = cellViewModel.id + let cachedViewController = WebCacheManager.shared.viewControllerForKey(id) + + if let cachedViewController = cachedViewController, cachedViewController.errorViewController?.isHandlingError == nil { + return cachedViewController + } + + if cachedViewController?.errorViewController?.isHandlingError ?? false { + WebCacheManager.shared.removeViewControllerForKey(id) + } + + let newViewController = createWebViewController(with: cellViewModel) + WebCacheManager.shared.cacheData(forKey: id, viewController: newViewController) + return newViewController + } + + private func createWebViewController(with viewModel: BoxListCellViewModel) -> WebViewController { + let viewController = WebViewController() + viewController.selectedWebsite = viewModel.url + viewController.title = viewModel.name + viewController.id = viewModel.id + return viewController + } + func tableView(_ tableView: UITableView, previewForDismissingContextMenuWithConfiguration configuration: UIContextMenuConfiguration) -> UITargetedPreview? { guard let indexPath = configuration.identifier as? IndexPath, let cell = tableView.cellForRow(at: indexPath) else { return nil diff --git a/iBox/Sources/BoxList/BoxListViewController.swift b/iBox/Sources/BoxList/BoxListViewController.swift index 87ba714..120d6d6 100644 --- a/iBox/Sources/BoxList/BoxListViewController.swift +++ b/iBox/Sources/BoxList/BoxListViewController.swift @@ -43,6 +43,19 @@ class BoxListViewController: BaseViewController, BaseViewController contentView.viewModel?.input.send(.viewWillAppear) } + override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { + super.viewWillTransition(to: size, with: coordinator) + coordinator.animate(alongsideTransition: nil) { _ in + self.dismissPreviewIfNeeded() + } + } + + func dismissPreviewIfNeeded() { + if let previewVC = self.presentedViewController as? WebViewController { + previewVC.dismiss(animated: true, completion: nil) + } + } + // MARK: - BaseViewControllerProtocol func setupNavigationBar() { @@ -205,20 +218,33 @@ extension BoxListViewController: BoxListViewDelegate { self.present(controller, animated: true) } - func didSelectWeb(id: UUID, at url: URL, withName name: String) { - if let cachedViewController = WebCacheManager.shared.viewControllerForKey(id) { - // 이미 캐시에 존재한다면, 그 인스턴스를 재사용 - navigationController?.pushViewController(cachedViewController, animated: true) - } else { - // 캐시에 없는 경우, 새로운 viewController 인스턴스를 생성하고 캐시에 추가합니다. - let viewController = WebViewController() - viewController.delegate = self - viewController.selectedWebsite = url - viewController.title = name - WebCacheManager.shared.cacheData(forKey: id, viewController: viewController) - navigationController?.pushViewController(viewController, animated: true) + let viewController = getOrCreateWebViewController(id: id, url: url, name: name) + navigationController?.pushViewController(viewController, animated: true) + } + + private func getOrCreateWebViewController(id: UUID, url: URL, name: String) -> WebViewController { + let cachedViewController = WebCacheManager.shared.viewControllerForKey(id) + + if let cachedViewController = cachedViewController, cachedViewController.errorViewController?.isHandlingError == nil { + return cachedViewController + } + + if cachedViewController?.errorViewController?.isHandlingError ?? false { + WebCacheManager.shared.removeViewControllerForKey(id) } + + return createAndCacheWebViewController(id: id, url: url, name: name) + } + + private func createAndCacheWebViewController(id: UUID, url: URL, name: String) -> WebViewController { + let viewController = WebViewController() + viewController.delegate = self + viewController.selectedWebsite = url + viewController.title = name + viewController.id = id + WebCacheManager.shared.cacheData(forKey: id, viewController: viewController) + return viewController } func pushViewController(type: EditType) { diff --git a/iBox/Sources/CustomLaunchScreen/CustomLaunchScreenView.swift b/iBox/Sources/CustomLaunchScreen/CustomLaunchScreenView.swift index d1d0202..92876d2 100644 --- a/iBox/Sources/CustomLaunchScreen/CustomLaunchScreenView.swift +++ b/iBox/Sources/CustomLaunchScreen/CustomLaunchScreenView.swift @@ -63,7 +63,7 @@ class CustomLaunchScreenView: UIView { timer = Timer.scheduledTimer(withTimeInterval: 0.2, repeats: true) { [weak self] timer in guard let self = self else { return } - let state = AppStateManager.shared.isVersionCheckCompleted + let state = AppStateManager.shared.versionCheckCompleted if state == .success || state == .later || state == .maxRetryReached { timer.invalidate() self.timer = nil diff --git a/iBox/Sources/CustomLaunchScreen/CustomLaunchScreenViewController.swift b/iBox/Sources/CustomLaunchScreen/CustomLaunchScreenViewController.swift index 9251691..1c63d5b 100644 --- a/iBox/Sources/CustomLaunchScreen/CustomLaunchScreenViewController.swift +++ b/iBox/Sources/CustomLaunchScreen/CustomLaunchScreenViewController.swift @@ -38,7 +38,7 @@ class CustomLaunchScreenViewController: UIViewController { // MARK: - Custom Update Checker View (예정) private func observeVersionCheckCompletion() { - AppStateManager.shared.$isVersionCheckCompleted + AppStateManager.shared.$versionCheckCompleted .receive(on: DispatchQueue.main) .sink { [weak self] result in switch result { diff --git a/iBox/Sources/Error/ErrorCode.swift b/iBox/Sources/Error/ErrorCode.swift new file mode 100644 index 0000000..65a90fe --- /dev/null +++ b/iBox/Sources/Error/ErrorCode.swift @@ -0,0 +1,18 @@ +// +// ErrorCode.swift +// iBox +// +// Created by Chan on 4/23/24. +// + +import Foundation + +enum ViewErrorCode: Equatable { + case normal + case unknown + case webContentProcessTerminated + case webViewInvalidated + case javaScriptExceptionOccurred + case javaScriptResultTypeIsUnsupported + case networkError(URLError) +} diff --git a/iBox/Sources/Error/ErrorPageView.swift b/iBox/Sources/Error/ErrorPageView.swift index bfa39bd..1338548 100644 --- a/iBox/Sources/Error/ErrorPageView.swift +++ b/iBox/Sources/Error/ErrorPageView.swift @@ -12,38 +12,89 @@ import SnapKit class ErrorPageView: UIView { private var imageViews: [UIImageView] = [] private let images = ["fox_page0", "fox_page1", "fox_page2", "fox_page3", "fox_page4"] - private var timer: Timer? + var timer: Timer? - let messageLabel = UILabel() + private let backPannelView = UIView().then { + $0.backgroundColor = .backgroundColor + } + + private let messageLabel = UILabel().then { + $0.textAlignment = .center + $0.numberOfLines = 0 + $0.lineBreakMode = .byTruncatingTail + } + + private let problemLabel = UILabel().then { + $0.textAlignment = .center + $0.numberOfLines = 0 + $0.text = "해당 주소에서 문제가 발생했습니다." + } + + let backButton = UIButton().then { + $0.setTitle("나가기", for: .normal) + $0.backgroundColor = .box2 + $0.setTitleColor(.white, for: .normal) + $0.layer.cornerRadius = 10 + $0.addAnimationForStateChange(from: .box, to: .box2) + } + + let retryButton = UIButton().then { + $0.setTitle("새로고침", for: .normal) + $0.backgroundColor = .systemGray + $0.setTitleColor(.white, for: .normal) + $0.layer.cornerRadius = 10 + $0.addAnimationForStateChange(from: .box, to: .systemGray) + } + + let closeButton = UIButton().then { + $0.setTitle("닫기", for: .normal) + $0.backgroundColor = .systemGray + $0.setTitleColor(.white, for: .normal) + $0.layer.cornerRadius = 10 + $0.addAnimationForStateChange(from: .box, to: .systemGray) + } - let backButton = UIButton() - let retryButton = UIButton() - override init(frame: CGRect) { super.init(frame: frame) - setupViews() + setupProperty() + setupHierarchy() + setupAnimation() setupLayout() changeImages() } + override func willMove(toSuperview newSuperview: UIView?) { + super.willMove(toSuperview: newSuperview) + + if newSuperview == nil { + timer?.invalidate() + timer = nil + } + } + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - private func setupViews() { - backgroundColor = .clear - - messageLabel.textAlignment = .center - messageLabel.numberOfLines = 0 - - retryButton.setTitle("무시하기", for: .normal) - retryButton.backgroundColor = .systemBlue - retryButton.setTitleColor(.white, for: .normal) - retryButton.layer.cornerRadius = 10 - - addSubview(messageLabel) - addSubview(retryButton) - + override func layoutSubviews() { + super.layoutSubviews() + backPannelView.roundCorners([.bottomLeft, .bottomRight], radius: 20) + } + + private func setupProperty() { + changeImages() + } + + private func setupHierarchy() { + addSubview(backPannelView) + backPannelView.addSubview(retryButton) + backPannelView.addSubview(closeButton) + backPannelView.addSubview(backButton) + backPannelView.addSubview(problemLabel) + backPannelView.addSubview(messageLabel) + } + + private func setupAnimation() { for imageName in images { let imageView = UIImageView(image: UIImage(named: imageName)) imageView.contentMode = .scaleAspectFit @@ -52,40 +103,64 @@ class ErrorPageView: UIView { addSubview(imageView) imageViews.append(imageView) } - - changeImages() - } private func setupLayout() { - imageViews.forEach { imageView in - imageView.snp.makeConstraints { make in - make.centerX.equalToSuperview() - make.centerY.equalToSuperview() - make.leading.greaterThanOrEqualToSuperview().offset(20) - make.trailing.lessThanOrEqualToSuperview().offset(-20) - make.width.height.equalTo(32) - } + backPannelView.snp.makeConstraints { make in + make.top.equalToSuperview() + make.trailing.leading.equalToSuperview().inset(20) + make.height.equalToSuperview().multipliedBy(0.5) } - messageLabel.snp.makeConstraints { make in - make.top.equalTo(imageViews[0].snp.bottom).offset(20) + retryButton.snp.makeConstraints { make in + make.bottom.equalToSuperview().inset(10) make.centerX.equalToSuperview() - make.leading.greaterThanOrEqualToSuperview().offset(20) - make.trailing.lessThanOrEqualToSuperview().offset(-20) + make.width.equalTo(100) + make.height.equalTo(44) } - retryButton.snp.makeConstraints { make in - make.top.equalTo(messageLabel.snp.bottom).offset(20) - make.centerX.equalToSuperview() + closeButton.snp.makeConstraints { make in + make.leading.equalTo(retryButton.snp.trailing).offset(20) + make.centerY.equalTo(retryButton.snp.centerY) make.width.equalTo(100) make.height.equalTo(44) } + + backButton.snp.makeConstraints { make in + make.trailing.equalTo(retryButton.snp.leading).offset(-20) + make.centerY.equalTo(retryButton.snp.centerY) + make.width.equalTo(100) + make.height.equalTo(44) + } + + problemLabel.snp.makeConstraints { make in + make.bottom.equalTo(retryButton.snp.top).offset(-10) + make.leading.equalToSuperview().offset(20) + make.trailing.equalToSuperview().offset(-20) + } + + messageLabel.snp.makeConstraints { make in + make.centerX.equalToSuperview() + make.bottom.equalTo(problemLabel.snp.top).offset(-10) + make.leading.equalToSuperview().offset(20) + make.trailing.equalToSuperview().offset(-20) + } + + imageViews.forEach { imageView in + imageView.snp.makeConstraints { make in + make.centerX.equalToSuperview() + make.top.equalTo(safeAreaLayoutGuide).offset(10) + make.bottom.equalTo(messageLabel.snp.top).offset(-10) + make.leading.greaterThanOrEqualToSuperview().offset(20) + make.trailing.lessThanOrEqualToSuperview().offset(-20) + make.width.height.equalTo(32) + } + } } func configure(with error: Error, url: String) { - messageLabel.text = "\(url): \n해당 주소를 불러오는데 실패했어요!" + messageLabel.text = "\(url)" print(error.localizedDescription) } @@ -93,12 +168,14 @@ class ErrorPageView: UIView { var currentIndex = 0 timer = Timer.scheduledTimer(withTimeInterval: 0.2, repeats: true) { [weak self] timer in - guard let self = self else { return } - - let state = AppStateManager.shared.isVersionCheckCompleted - if state == .success || state == .later || state == .maxRetryReached { + guard let self = self else { timer.invalidate() - self.timer = nil + return + } + + let state = AppStateManager.shared.currentViewErrorState + + if state == .normal { return } diff --git a/iBox/Sources/Error/ErrorPageViewController.swift b/iBox/Sources/Error/ErrorPageViewController.swift index 04837c5..0cc570e 100644 --- a/iBox/Sources/Error/ErrorPageViewController.swift +++ b/iBox/Sources/Error/ErrorPageViewController.swift @@ -10,6 +10,7 @@ import WebKit protocol ErrorPageControllerDelegate: AnyObject { func presentErrorPage(_ errorPage: ErrorPageViewController) + func backButton() } protocol WebViewErrorDelegate { @@ -19,7 +20,10 @@ protocol WebViewErrorDelegate { class ErrorPageViewController: UIViewController { weak var delegate: ErrorPageControllerDelegate? var webView: WebView? - + var isHandlingError = false + + let slideInPresentationManager = SlideInPresentationManager() + init(webView: WebView) { super.init(nibName: nil, bundle: nil) self.webView = webView @@ -35,11 +39,27 @@ class ErrorPageViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() + setupProperty() + setupPresentation() + } + + override func viewWillDisappear(_ animated: Bool) { + resetErrorHandling() + } + + private func setupProperty() { if let errorPageView = view as? ErrorPageView { errorPageView.retryButton.addTarget(self, action: #selector(retryButtonTapped), for: .touchUpInside) + errorPageView.closeButton.addTarget(self, action: #selector(closeButtonTapped), for: .touchUpInside) + errorPageView.backButton.addTarget(self, action: #selector(backButtonTapped), for: .touchUpInside) } } + func setupPresentation() { + self.modalPresentationStyle = .custom + self.transitioningDelegate = slideInPresentationManager + } + func configureWithError(_ error: Error, url: String) { if let errorPageView = view as? ErrorPageView { errorPageView.configure(with: error, url: url) @@ -48,20 +68,87 @@ class ErrorPageViewController: UIViewController { @objc private func retryButtonTapped() { webView?.retryLoading() + } + + @objc private func closeButtonTapped() { dismiss(animated: true) } + @objc func backButtonTapped() { + self.dismiss(animated: true) { + self.delegate?.backButton() + } + } + func handleError(_ error: Error, _ url: URL?) { - self.modalPresentationStyle = .overFullScreen - self.configureWithError(error, url: url?.absoluteString ?? "") - delegate?.presentErrorPage(self) + guard !isHandlingError else { return } + isHandlingError = true + + if presentedViewController != nil { + dismiss(animated: true) { + self.configureWithError(error, url: url?.absoluteString ?? "") + self.delegate?.presentErrorPage(self) + } + } else { + configureWithError(error, url: url?.absoluteString ?? "") + delegate?.presentErrorPage(self) + } + } + + func resetErrorHandling() { + isHandlingError = false + } + + private func convertErrorToViewErrorCode(_ error: Error) -> ViewErrorCode { + if let wkError = error as? WKError { + switch wkError.code { + case .webContentProcessTerminated: + return .webContentProcessTerminated + case .webViewInvalidated: + return .webViewInvalidated + case .javaScriptExceptionOccurred: + return .javaScriptExceptionOccurred + case .javaScriptResultTypeIsUnsupported: + return .javaScriptResultTypeIsUnsupported + default: + return .unknown + } + } else if let urlError = error as? URLError { + return .networkError(urlError) + } + + return .unknown + } + + private func handleViewError(_ error: ViewErrorCode) { + switch error { + case .normal: + print("OK.") + case .unknown: + print("Unknown error occurred in the view.") + case .webContentProcessTerminated: + print("Web content process has been terminated unexpectedly.") + case .webViewInvalidated: + print("The web view has been invalidated.") + case .javaScriptExceptionOccurred: + print("A JavaScript exception occurred.") + case .javaScriptResultTypeIsUnsupported: + print("JavaScript returned a result type that is not supported.") + case .networkError(let urlError): + print("Network error occurred: \(urlError.localizedDescription)") + } + + AppStateManager.shared.updateViewError(error) } } extension ErrorPageViewController: WebViewErrorDelegate { func webView(_ webView: WebView, didFailWithError error: Error, url: URL?) { - handleError(error, url) + handleError(error, url) + + let viewErrorCode = convertErrorToViewErrorCode(error) + handleViewError(viewErrorCode) } } diff --git a/iBox/Sources/Extension/UIButton+Extension.swift b/iBox/Sources/Extension/UIButton+Extension.swift new file mode 100644 index 0000000..16dee87 --- /dev/null +++ b/iBox/Sources/Extension/UIButton+Extension.swift @@ -0,0 +1,55 @@ +// +// UIButton+Extension.swift +// iBox +// +// Created by Chan on 4/25/24. +// + +import UIKit +import ObjectiveC + +private var touchDownColorKey: UInt8 = 0 +private var touchUpColorKey: UInt8 = 0 + +extension UIButton { + var touchDownColor: UIColor? { + get { + return objc_getAssociatedObject(self, &touchDownColorKey) as? UIColor + } + set { + objc_setAssociatedObject(self, &touchDownColorKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) + } + } + + var touchUpColor: UIColor? { + get { + return objc_getAssociatedObject(self, &touchUpColorKey) as? UIColor + } + set { + objc_setAssociatedObject(self, &touchUpColorKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) + } + } + + func addAnimationForStateChange(from: UIColor, to: UIColor) { + self.touchDownColor = from + self.touchUpColor = to + addTarget(self, action: #selector(animateOnTouchDown), for: .touchDown) + addTarget(self, action: #selector(animateOnTouchUp), for: [.touchUpInside, .touchUpOutside, .touchCancel]) + } + + @objc private func animateOnTouchDown() { + if let color = touchDownColor { + UIView.animate(withDuration: 0.3) { + self.backgroundColor = color + } + } + } + + @objc private func animateOnTouchUp() { + if let color = touchUpColor { + UIView.animate(withDuration: 0.3) { + self.backgroundColor = color + } + } + } +} diff --git a/iBox/Sources/Extension/UIColor+Extension.swift b/iBox/Sources/Extension/UIColor+Extension.swift index f4c0fc4..2a5fc3f 100644 --- a/iBox/Sources/Extension/UIColor+Extension.swift +++ b/iBox/Sources/Extension/UIColor+Extension.swift @@ -32,6 +32,8 @@ extension UIColor { static let box = UIColor(hex: 0xFF7F29) static let box2 = UIColor(hex: 0xFF9548) static let box3 = UIColor(hex: 0xFFDC6E) + static let boxWithOpacity = UIColor(hex: 0xFF7F29, alpha: 0.7) + static let box2WithOpacity = UIColor(hex: 0xFF9548, alpha: 0.7) static let tableViewBackgroundColor = color(light: .systemGroupedBackground, dark: .systemGray4) static let folderGray = color(light: .systemGray3, dark: .systemGray2) static let webIconColor = color(light: .black, dark: .systemGray) diff --git a/iBox/Sources/Extension/UIView+Extension.swift b/iBox/Sources/Extension/UIView+Extension.swift index 890082d..e7f584c 100644 --- a/iBox/Sources/Extension/UIView+Extension.swift +++ b/iBox/Sources/Extension/UIView+Extension.swift @@ -30,6 +30,16 @@ extension UIView { } } + func roundCorners(_ corners: UIRectCorner, radius: CGFloat) { + DispatchQueue.main.async { // 확실히 메인 스레드에서 실행되도록 강제 + let path = UIBezierPath(roundedRect: self.bounds, byRoundingCorners: corners, cornerRadii: CGSize(width: radius, height: radius)) + let mask = CAShapeLayer() + mask.path = path.cgPath + mask.frame = self.bounds + self.layer.mask = mask + } + } + // MARK: - 뷰 계층 구조 log func printViewHierarchy(level: Int = 0) { let padding = String(repeating: " ", count: level * 2) @@ -40,4 +50,5 @@ extension UIView { subview.printViewHierarchy(level: level + 1) } } + } diff --git a/iBox/Sources/Settings/Reset/ResetSuccessView.swift b/iBox/Sources/Settings/Reset/ResetSuccessView.swift index a52388b..c72685d 100644 --- a/iBox/Sources/Settings/Reset/ResetSuccessView.swift +++ b/iBox/Sources/Settings/Reset/ResetSuccessView.swift @@ -75,6 +75,7 @@ class ResetSuccessView: UIView { make.width.height.equalTo(50) } } + private func animateView() { UIView.animate(withDuration: 0.5, animations: { self.alpha = 1.0 diff --git a/iBox/Sources/Shared/Animator/FullSizePresentationController.swift b/iBox/Sources/Shared/Animator/FullSizePresentationController.swift new file mode 100644 index 0000000..8693cab --- /dev/null +++ b/iBox/Sources/Shared/Animator/FullSizePresentationController.swift @@ -0,0 +1,37 @@ +// +// FullSizePresentationController.swift +// iBox +// +// Created by Chan on 4/24/24. +// + +import UIKit + +class FullSizePresentationController: UIPresentationController { + private var dimmingView: UIView! + + override func containerViewDidLayoutSubviews() { + super.containerViewDidLayoutSubviews() + + dimmingView.frame = containerView?.bounds ?? CGRect.zero + presentedView?.frame = frameOfPresentedViewInContainerView + } + + override func presentationTransitionWillBegin() { + guard let containerView = containerView else { return } + + dimmingView = UIView(frame: containerView.bounds) + dimmingView.backgroundColor = UIColor(white: 0.0, alpha: 0.4) + dimmingView.autoresizingMask = [.flexibleWidth, .flexibleHeight] + + containerView.addSubview(dimmingView) + containerView.sendSubviewToBack(dimmingView) + + super.presentationTransitionWillBegin() + } + + override var frameOfPresentedViewInContainerView: CGRect { + guard let containerView = containerView else { return .zero } + return CGRect(x: 0, y: 0, width: containerView.bounds.width, height: containerView.bounds.height) + } +} diff --git a/iBox/Sources/Shared/Animator/SlideInPresentationAnimator.swift b/iBox/Sources/Shared/Animator/SlideInPresentationAnimator.swift new file mode 100644 index 0000000..f003178 --- /dev/null +++ b/iBox/Sources/Shared/Animator/SlideInPresentationAnimator.swift @@ -0,0 +1,46 @@ +// +// SlideInPresentationAnimator.swift +// iBox +// +// Created by Chan on 4/24/24. +// + +import UIKit + +class SlideInPresentationAnimator: NSObject, UIViewControllerAnimatedTransitioning { + let isPresentation: Bool + + init(isPresentation: Bool) { + self.isPresentation = isPresentation + super.init() + } + + func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval { + return 0.3 + } + + func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { + let key = isPresentation ? UITransitionContextViewControllerKey.to : UITransitionContextViewControllerKey.from + guard let controller = transitionContext.viewController(forKey: key) else { return } + + if isPresentation { + transitionContext.containerView.addSubview(controller.view) + } + + let presentedFrame = transitionContext.finalFrame(for: controller) + var dismissedFrame = presentedFrame + dismissedFrame.origin.y = -presentedFrame.height + + let initialFrame = isPresentation ? dismissedFrame : presentedFrame + let finalFrame = isPresentation ? presentedFrame : dismissedFrame + + controller.view.frame = initialFrame + UIView.animate( + withDuration: transitionDuration(using: transitionContext), + animations: { + controller.view.frame = finalFrame + }, completion: { finished in + transitionContext.completeTransition(finished) + }) + } +} diff --git a/iBox/Sources/Shared/AppStateManager.swift b/iBox/Sources/Shared/AppStateManager.swift index 3c32cda..10f8b22 100644 --- a/iBox/Sources/Shared/AppStateManager.swift +++ b/iBox/Sources/Shared/AppStateManager.swift @@ -10,6 +10,10 @@ import Combine class AppStateManager { static let shared = AppStateManager() - @Published var isVersionCheckCompleted: VersionCheckCode = .initial - + @Published var versionCheckCompleted: VersionCheckCode = .initial + var currentViewErrorState: ViewErrorCode = .normal + + func updateViewError(_ error: ViewErrorCode) { + currentViewErrorState = error + } } diff --git a/iBox/Sources/Shared/SlideInPresentationManager.swift b/iBox/Sources/Shared/SlideInPresentationManager.swift new file mode 100644 index 0000000..6e51d0f --- /dev/null +++ b/iBox/Sources/Shared/SlideInPresentationManager.swift @@ -0,0 +1,22 @@ +// +// SlideInPresentationManager.swift +// iBox +// +// Created by Chan on 4/23/24. +// + +import UIKit + +class SlideInPresentationManager: NSObject, UIViewControllerTransitioningDelegate { + func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? { + return FullSizePresentationController(presentedViewController: presented, presenting: presenting) + } + + func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? { + return SlideInPresentationAnimator(isPresentation: true) + } + + func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? { + return SlideInPresentationAnimator(isPresentation: false) + } +} diff --git a/iBox/Sources/Web/WebViewController.swift b/iBox/Sources/Web/WebViewController.swift index c67e266..b872a1a 100644 --- a/iBox/Sources/Web/WebViewController.swift +++ b/iBox/Sources/Web/WebViewController.swift @@ -13,7 +13,8 @@ protocol WebViewDelegate { } class WebViewController: BaseViewController, BaseViewControllerProtocol { - + var id: UUID? + var slideInPresentationManager = SlideInPresentationManager() var errorViewController: ErrorPageViewController? var delegate: AddBookmarkViewControllerProtocol? var selectedWebsite: URL? @@ -34,6 +35,21 @@ class WebViewController: BaseViewController, BaseViewControllerProtocol contentView.setupRefreshControl() } + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + + if ((isMovingFromParent || isBeingDismissed) && AppStateManager.shared.currentViewErrorState != .normal){ + if let id = self.id { + WebCacheManager.shared.removeViewControllerForKey(id) + } + } + } + + deinit { + AppStateManager.shared.currentViewErrorState = .normal + errorViewController = nil + } + // MARK: - BaseViewControllerProtocol func setupNavigationBar() { @@ -72,4 +88,12 @@ extension WebViewController: ErrorPageControllerDelegate { func presentErrorPage(_ errorPage: ErrorPageViewController) { self.present(errorPage, animated: true, completion: nil) } + + func backButton() { + if let navController = navigationController { + navController.popViewController(animated: true) + } else { + dismiss(animated: true) + } + } }