// // BrowserNavigationController.swift // Gemini-iOS // // Created by Shadowfacts on 12/17/20. // import UIKit import BrowserCore import Combine class BrowserNavigationController: UIViewController { let navigator: NavigationManager private var backBrowserVCs = [BrowserWebViewController]() private var forwardBrowserVCs = [BrowserWebViewController]() private var currentBrowserVC: BrowserWebViewController! private var gestureState: GestureState? private var cancellables = [AnyCancellable]() init(navigator: NavigationManager) { self.navigator = navigator super.init(nibName: nil, bundle: nil) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func viewDidLoad() { super.viewDidLoad() view.backgroundColor = .systemBackground currentBrowserVC = BrowserWebViewController(navigator: navigator, url: navigator.currentURL) embedChild(currentBrowserVC) navigator.navigationOperation .sink(receiveValue: self.onNavigate) .store(in: &cancellables) view.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(panGestureRecognized))) } private func onNavigate(_ operation: NavigationManager.Operation) { let newVC: BrowserWebViewController switch operation { case .go: backBrowserVCs.append(currentBrowserVC) newVC = BrowserWebViewController(navigator: navigator, url: navigator.currentURL) case let .backward(count: count): var removed = backBrowserVCs.suffix(count) backBrowserVCs.removeLast(count) forwardBrowserVCs.insert(currentBrowserVC, at: 0) newVC = removed.removeFirst() forwardBrowserVCs.insert(contentsOf: removed, at: 0) case let .forward(count: count): var removed = forwardBrowserVCs.prefix(count) forwardBrowserVCs.removeFirst(count) backBrowserVCs.append(currentBrowserVC) newVC = removed.removeFirst() backBrowserVCs.append(contentsOf: removed) } currentBrowserVC.removeViewAndController() currentBrowserVC = newVC embedChild(newVC) } private let startEdgeNavigationSwipeDistance: CGFloat = 75 private let finishEdgeNavigationVelocityThreshold: CGFloat = 500 private let edgeNavigationMaxDimmingAlpha: CGFloat = 0.35 private let edgeNavigationParallaxFactor: CGFloat = 0.25 private let totalEdgeNavigationTime: TimeInterval = 0.4 @objc private func panGestureRecognized(_ recognizer: UIPanGestureRecognizer) { let location = recognizer.location(in: view) let velocity = recognizer.velocity(in: view) switch recognizer.state { case .began: if location.x < startEdgeNavigationSwipeDistance && velocity.x > 0 && navigator.backStack.count > 0 { let older = backBrowserVCs.last ?? BrowserWebViewController(navigator: navigator, url: navigator.backStack.last!) embedChild(older) older.view.layer.zPosition = -2 older.view.transform = CGAffineTransform(translationX: -1 * edgeNavigationParallaxFactor * view.bounds.width, y: 0) let dimmingView = UIView() dimmingView.translatesAutoresizingMaskIntoConstraints = false dimmingView.backgroundColor = .black dimmingView.layer.zPosition = -1 dimmingView.alpha = edgeNavigationMaxDimmingAlpha view.embedSubview(dimmingView) let animator = UIViewPropertyAnimator(duration: totalEdgeNavigationTime, curve: .easeInOut) { dimmingView.alpha = 0 older.view.transform = .identity self.currentBrowserVC.view.transform = CGAffineTransform(translationX: self.view.bounds.width, y: 0) } animator.addCompletion { (position) in dimmingView.removeFromSuperview() older.view.transform = .identity older.view.layer.zPosition = 0 older.removeViewAndController() self.currentBrowserVC.view.transform = .identity if position == .end { self.navigator.back() } } gestureState = .backwards(animator) } else if location.x > view.bounds.width - startEdgeNavigationSwipeDistance && velocity.x < 0 && navigator.forwardStack.count > 0 { let newer = forwardBrowserVCs.first ?? BrowserWebViewController(navigator: navigator, url: navigator.backStack.first!) embedChild(newer) newer.view.transform = CGAffineTransform(translationX: view.bounds.width, y: 0) newer.view.layer.zPosition = 2 let dimmingView = UIView() dimmingView.translatesAutoresizingMaskIntoConstraints = false dimmingView.backgroundColor = .black dimmingView.layer.zPosition = 1 dimmingView.alpha = 0 view.embedSubview(dimmingView) let animator = UIViewPropertyAnimator(duration: totalEdgeNavigationTime, curve: .easeInOut) { dimmingView.alpha = self.edgeNavigationMaxDimmingAlpha newer.view.transform = .identity self.currentBrowserVC.view.transform = CGAffineTransform(translationX: -1 * self.edgeNavigationParallaxFactor * self.view.bounds.width, y: 0) } animator.addCompletion { (position) in dimmingView.removeFromSuperview() newer.removeViewAndController() newer.view.layer.zPosition = 0 newer.view.transform = .identity self.currentBrowserVC.view.transform = .identity if position == .end { self.navigator.forward() } } gestureState = .forwards(animator) } case .changed: let translation = recognizer.translation(in: view) switch gestureState { case let .backwards(animator): animator.fractionComplete = translation.x / view.bounds.width case let .forwards(animator): animator.fractionComplete = abs(translation.x) / view.bounds.width case nil: break } case .ended, .cancelled: switch gestureState { case let .backwards(animator): let shouldComplete = location.x > view.bounds.width / 2 || velocity.x > finishEdgeNavigationVelocityThreshold animator.isReversed = !shouldComplete animator.startAnimation() case let .forwards(animator): let shouldComplete = location.x < view.bounds.width / 2 || velocity.x < -finishEdgeNavigationVelocityThreshold animator.isReversed = !shouldComplete animator.startAnimation() case nil: break } gestureState = nil default: return } } } extension BrowserNavigationController { enum GestureState { case backwards(UIViewPropertyAnimator) case forwards(UIViewPropertyAnimator) } }