Gemini/Gemini-iOS/BrowserViewController.swift

165 lines
6.5 KiB
Swift

//
// 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<BrowserView>!
private var navBarHost: UIHostingController<NavigationBar>!
private var toolBarHost: UIHostingController<ToolBar>!
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()
}
}
}