// // BrowserViewController.swift // Gemini-iOS // // Created by Shadowfacts on 9/28/20. // import UIKit import SwiftUI import BrowserCore import Combine class BrowserViewController: UIViewController, UIScrollViewDelegate { let navigator: NavigationManager private var scrollView: UIScrollView! private var browserHost: UIHostingController! private var navBarHost: UIHostingController! private var toolBarHost: UIHostingController! private var prevScrollViewContentOffset: CGPoint? private var barAnimator: UIViewPropertyAnimator? 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 scrollView = UIScrollView() scrollView.translatesAutoresizingMaskIntoConstraints = false scrollView.keyboardDismissMode = .interactive view.addSubview(scrollView) scrollView.delegate = self NSLayoutConstraint.activate([ scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor), scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor), scrollView.topAnchor.constraint(equalTo: view.topAnchor), scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor), ]) browserHost = UIHostingController(rootView: BrowserView(navigator: navigator, scrollingEnabled: false)) browserHost.view.translatesAutoresizingMaskIntoConstraints = false scrollView.addSubview(browserHost.view) addChild(browserHost) browserHost.didMove(toParent: self) NSLayoutConstraint.activate([ scrollView.contentLayoutGuide.leadingAnchor.constraint(equalTo: browserHost.view.leadingAnchor), scrollView.contentLayoutGuide.trailingAnchor.constraint(equalTo: browserHost.view.trailingAnchor), scrollView.contentLayoutGuide.topAnchor.constraint(equalTo: browserHost.view.topAnchor), scrollView.contentLayoutGuide.bottomAnchor.constraint(equalTo: browserHost.view.bottomAnchor), browserHost.view.widthAnchor.constraint(equalTo: view.widthAnchor), // make sure the browser host view is at least the screen height so the loading indicator appears centered browserHost.view.heightAnchor.constraint(greaterThanOrEqualTo: view.heightAnchor), ]) navBarHost = UIHostingController(rootView: NavigationBar(navigator: navigator)) navBarHost.view.translatesAutoresizingMaskIntoConstraints = false view.addSubview(navBarHost.view) addChild(navBarHost) navBarHost.didMove(toParent: self) NSLayoutConstraint.activate([ navBarHost.view.leadingAnchor.constraint(equalTo: view.leadingAnchor), navBarHost.view.trailingAnchor.constraint(equalTo: view.trailingAnchor), navBarHost.view.topAnchor.constraint(equalTo: view.topAnchor), ]) toolBarHost = UIHostingController(rootView: ToolBar(navigator: navigator)) toolBarHost.view.translatesAutoresizingMaskIntoConstraints = false view.addSubview(toolBarHost.view) addChild(toolBarHost) toolBarHost.didMove(toParent: self) NSLayoutConstraint.activate([ toolBarHost.view.leadingAnchor.constraint(equalTo: view.leadingAnchor), toolBarHost.view.trailingAnchor.constraint(equalTo: view.trailingAnchor), toolBarHost.view.bottomAnchor.constraint(equalTo: view.bottomAnchor), ]) navigator.$currentURL .sink { (_) in self.scrollView.contentOffset = .zero self.navBarHost.view.transform = .identity self.toolBarHost.view.transform = .identity } .store(in: &cancellables) } override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() let insets = UIEdgeInsets( top: navBarHost.view.bounds.height - view.safeAreaInsets.top, left: 0, bottom: toolBarHost.view.bounds.height - view.safeAreaInsets.bottom, right: 0 ) scrollView.contentInset = insets scrollView.scrollIndicatorInsets = insets } // MARK: - UIScrollViewDelegate func scrollViewDidScroll(_ scrollView: UIScrollView) { var scrollViewDelta: CGFloat = 0 if let prev = prevScrollViewContentOffset { scrollViewDelta = scrollView.contentOffset.y - prev.y } prevScrollViewContentOffset = scrollView.contentOffset // When certain state changes happen, the scroll view seems to "scroll" by top the safe area inset. // It's not actually user scrolling, and this screws up our animation, so we ignore it. guard abs(scrollViewDelta) != view.safeAreaInsets.top, scrollViewDelta != 0, scrollView.contentOffset.y > 0 else { return } let barAnimator: UIViewPropertyAnimator if let animator = self.barAnimator { barAnimator = animator } else { navBarHost.view.transform = .identity toolBarHost.view.transform = .identity barAnimator = UIViewPropertyAnimator(duration: 0.2, curve: .linear) { self.navBarHost.view.transform = CGAffineTransform(translationX: 0, y: -self.navBarHost.view.frame.height) self.toolBarHost.view.transform = CGAffineTransform(translationX: 0, y: self.toolBarHost.view.frame.height) } if scrollViewDelta < 0 { barAnimator.fractionComplete = 1 } barAnimator.addCompletion { (_) in self.barAnimator = nil } self.barAnimator = barAnimator } let progressDelta = scrollViewDelta / navBarHost.view.bounds.height barAnimator.fractionComplete = max(0, min(1, barAnimator.fractionComplete + progressDelta)) } func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { if let barAnimator = barAnimator { if barAnimator.fractionComplete < 0.5 { barAnimator.isReversed = true } barAnimator.startAnimation() } } }