165 lines
6.5 KiB
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()
|
|
}
|
|
}
|
|
|
|
}
|