Replace UINavigationController with custom navigation controller
This commit is contained in:
parent
314d8cf82c
commit
abb80df9a7
|
@ -6,6 +6,7 @@
|
||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
import Combine
|
||||||
|
|
||||||
public protocol NavigationManagerDelegate: class {
|
public protocol NavigationManagerDelegate: class {
|
||||||
func loadNonGeminiURL(_ url: URL)
|
func loadNonGeminiURL(_ url: URL)
|
||||||
|
@ -19,6 +20,8 @@ public class NavigationManager: NSObject, ObservableObject {
|
||||||
@Published public var backStack = [URL]()
|
@Published public var backStack = [URL]()
|
||||||
@Published public var forwardStack = [URL]()
|
@Published public var forwardStack = [URL]()
|
||||||
|
|
||||||
|
public let navigationOperation = PassthroughSubject<Operation, Never>()
|
||||||
|
|
||||||
public var displayURL: String {
|
public var displayURL: String {
|
||||||
var components = URLComponents(url: currentURL, resolvingAgainstBaseURL: false)!
|
var components = URLComponents(url: currentURL, resolvingAgainstBaseURL: false)!
|
||||||
if components.port == 1965 {
|
if components.port == 1965 {
|
||||||
|
@ -58,6 +61,8 @@ public class NavigationManager: NSObject, ObservableObject {
|
||||||
backStack.append(currentURL)
|
backStack.append(currentURL)
|
||||||
currentURL = url
|
currentURL = url
|
||||||
forwardStack = []
|
forwardStack = []
|
||||||
|
|
||||||
|
navigationOperation.send(.go)
|
||||||
}
|
}
|
||||||
|
|
||||||
public func reload() {
|
public func reload() {
|
||||||
|
@ -66,9 +71,7 @@ public class NavigationManager: NSObject, ObservableObject {
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc public func back() {
|
@objc public func back() {
|
||||||
guard !backStack.isEmpty else { return }
|
back(count: 1)
|
||||||
forwardStack.insert(currentURL, at: 0)
|
|
||||||
currentURL = backStack.removeLast()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public func back(count: Int) {
|
public func back(count: Int) {
|
||||||
|
@ -78,12 +81,12 @@ public class NavigationManager: NSObject, ObservableObject {
|
||||||
forwardStack.insert(currentURL, at: 0)
|
forwardStack.insert(currentURL, at: 0)
|
||||||
currentURL = removed.removeFirst()
|
currentURL = removed.removeFirst()
|
||||||
forwardStack.insert(contentsOf: removed, at: 0)
|
forwardStack.insert(contentsOf: removed, at: 0)
|
||||||
|
|
||||||
|
navigationOperation.send(.backward(count: count))
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc public func forward() {
|
@objc public func forward() {
|
||||||
guard !forwardStack.isEmpty else { return }
|
forward(count: 1)
|
||||||
backStack.append(currentURL)
|
|
||||||
currentURL = forwardStack.removeFirst()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public func forward(count: Int) {
|
public func forward(count: Int) {
|
||||||
|
@ -93,6 +96,14 @@ public class NavigationManager: NSObject, ObservableObject {
|
||||||
backStack.append(currentURL)
|
backStack.append(currentURL)
|
||||||
currentURL = removed.removeLast()
|
currentURL = removed.removeLast()
|
||||||
backStack.append(contentsOf: removed)
|
backStack.append(contentsOf: removed)
|
||||||
|
|
||||||
|
navigationOperation.send(.forward(count: count))
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public extension NavigationManager {
|
||||||
|
enum Operation {
|
||||||
|
case go, forward(count: Int), backward(count: Int)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -2,21 +2,22 @@
|
||||||
// BrowserNavigationController.swift
|
// BrowserNavigationController.swift
|
||||||
// Gemini-iOS
|
// Gemini-iOS
|
||||||
//
|
//
|
||||||
// Created by Shadowfacts on 12/16/20.
|
// Created by Shadowfacts on 12/17/20.
|
||||||
//
|
//
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
import BrowserCore
|
import BrowserCore
|
||||||
import Combine
|
import Combine
|
||||||
|
|
||||||
class BrowserNavigationController: UINavigationController {
|
class BrowserNavigationController: UIViewController {
|
||||||
|
|
||||||
let navigator: NavigationManager
|
let navigator: NavigationManager
|
||||||
|
|
||||||
var poppedViewControllers = [UIViewController]()
|
private var backBrowserVCs = [BrowserWebViewController]()
|
||||||
var skipResetPoppedOnNextPush = false
|
private var forwardBrowserVCs = [BrowserWebViewController]()
|
||||||
|
private var currentBrowserVC: BrowserWebViewController!
|
||||||
|
|
||||||
private var interactivePushTransition: InteractivePushTransition!
|
private var gestureState: GestureState?
|
||||||
|
|
||||||
private var cancellables = [AnyCancellable]()
|
private var cancellables = [AnyCancellable]()
|
||||||
|
|
||||||
|
@ -24,74 +25,169 @@ class BrowserNavigationController: UINavigationController {
|
||||||
self.navigator = navigator
|
self.navigator = navigator
|
||||||
|
|
||||||
super.init(nibName: nil, bundle: nil)
|
super.init(nibName: nil, bundle: nil)
|
||||||
|
|
||||||
navigator.$currentURL
|
|
||||||
.sink { [weak self] (newURL) in
|
|
||||||
self?.pushViewController(BrowserWebViewController(navigator: navigator, url: newURL), animated: true)
|
|
||||||
}
|
|
||||||
.store(in: &cancellables)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
required init?(coder aDecoder: NSCoder) {
|
required init?(coder: NSCoder) {
|
||||||
fatalError("init(coder:) has not been implemented")
|
fatalError("init(coder:) has not been implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
|
|
||||||
interactivePushTransition = InteractivePushTransition(navigationController: self)
|
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)))
|
||||||
}
|
}
|
||||||
|
|
||||||
override func popViewController(animated: Bool) -> UIViewController? {
|
private func onNavigate(_ operation: NavigationManager.Operation) {
|
||||||
if let popped = super.popViewController(animated: animated) {
|
let newVC: BrowserWebViewController
|
||||||
poppedViewControllers.insert(popped, at: 0)
|
|
||||||
return popped
|
switch operation {
|
||||||
} else {
|
case .go:
|
||||||
return nil
|
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
override func popToRootViewController(animated: Bool) -> [UIViewController]? {
|
private let startEdgeNavigationSwipeDistance: CGFloat = 75
|
||||||
if let popped = super.popToRootViewController(animated: animated) {
|
private let finishEdgeNavigationVelocityThreshold: CGFloat = 500
|
||||||
poppedViewControllers = popped
|
private let edgeNavigationMaxDimmingAlpha: CGFloat = 0.35
|
||||||
return popped
|
private let edgeNavigationParallaxFactor: CGFloat = 0.25
|
||||||
} else {
|
private let totalEdgeNavigationTime: TimeInterval = 0.4
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override func popToViewController(_ viewController: UIViewController, animated: Bool) -> [UIViewController]? {
|
@objc private func panGestureRecognized(_ recognizer: UIPanGestureRecognizer) {
|
||||||
if let popped = super.popToViewController(viewController, animated: animated) {
|
let location = recognizer.location(in: view)
|
||||||
poppedViewControllers.insert(contentsOf: popped, at: 0)
|
let velocity = recognizer.velocity(in: view)
|
||||||
return popped
|
|
||||||
} else {
|
switch recognizer.state {
|
||||||
return nil
|
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)
|
||||||
override func pushViewController(_ viewController: UIViewController, animated: Bool) {
|
older.view.layer.zPosition = -2
|
||||||
if skipResetPoppedOnNextPush {
|
older.view.transform = CGAffineTransform(translationX: -1 * edgeNavigationParallaxFactor * view.bounds.width, y: 0)
|
||||||
skipResetPoppedOnNextPush = false
|
|
||||||
} else {
|
let dimmingView = UIView()
|
||||||
self.poppedViewControllers = []
|
dimmingView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
}
|
dimmingView.backgroundColor = .black
|
||||||
super.pushViewController(viewController, animated: animated)
|
dimmingView.layer.zPosition = -1
|
||||||
}
|
dimmingView.alpha = edgeNavigationMaxDimmingAlpha
|
||||||
|
view.embedSubview(dimmingView)
|
||||||
func onWillShow() {
|
let animator = UIViewPropertyAnimator(duration: totalEdgeNavigationTime, curve: .easeInOut) {
|
||||||
self.transitionCoordinator?.notifyWhenInteractionChanges({ (context) in
|
dimmingView.alpha = 0
|
||||||
if context.isCancelled {
|
older.view.transform = .identity
|
||||||
if self.interactivePushTransition.interactive {
|
self.currentBrowserVC.view.transform = CGAffineTransform(translationX: self.view.bounds.width, y: 0)
|
||||||
// when an interactive push gesture is cancelled, make sure to adding the VC that was being pushed back onto the popped stack so it doesn't disappear
|
|
||||||
self.poppedViewControllers.insert(self.interactivePushTransition.pushingViewController!, at: 0)
|
|
||||||
} else {
|
|
||||||
// when an interactive pop gesture is cancelled (i.e. the user lifts their finger before it triggers),
|
|
||||||
// the popViewController(animated:) method has already been called so the VC has already been added to the popped stack
|
|
||||||
// so we make sure to remove it, otherwise there could be duplicate VCs on the navigation stasck
|
|
||||||
self.poppedViewControllers.remove(at: 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)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -63,6 +63,8 @@ class BrowserWebViewController: UIViewController {
|
||||||
view.backgroundColor = .systemBackground
|
view.backgroundColor = .systemBackground
|
||||||
|
|
||||||
webView = WKWebView()
|
webView = WKWebView()
|
||||||
|
webView.backgroundColor = .systemBackground
|
||||||
|
webView.isOpaque = false
|
||||||
webView.navigationDelegate = self
|
webView.navigationDelegate = self
|
||||||
webView.translatesAutoresizingMaskIntoConstraints = false
|
webView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
view.addSubview(webView)
|
view.addSubview(webView)
|
||||||
|
|
|
@ -1,142 +0,0 @@
|
||||||
//
|
|
||||||
// InteractivePushTransition.swift
|
|
||||||
// Gemini-iOS
|
|
||||||
//
|
|
||||||
// Created by Shadowfacts on 12/16/20.
|
|
||||||
//
|
|
||||||
|
|
||||||
import UIKit
|
|
||||||
|
|
||||||
/// Allows interactively moving forward through the navigation stack after popping
|
|
||||||
/// Based on https://github.com/NSExceptional/TBInteractivePushTransition
|
|
||||||
class InteractivePushTransition: UIPercentDrivenInteractiveTransition {
|
|
||||||
|
|
||||||
fileprivate let minimumPushVelocityThreshold: CGFloat = 700
|
|
||||||
fileprivate let minimumPushDistanceThreshold: CGFloat = 0.5
|
|
||||||
fileprivate let pushAnimationDuration: TimeInterval = 0.35
|
|
||||||
|
|
||||||
private(set) weak var navigationController: BrowserNavigationController!
|
|
||||||
|
|
||||||
private(set) weak var interactivePushGestureRecognizer: UIScreenEdgePanGestureRecognizer!
|
|
||||||
|
|
||||||
private(set) var interactive = false
|
|
||||||
private(set) weak var pushingViewController: UIViewController?
|
|
||||||
|
|
||||||
init(navigationController: BrowserNavigationController) {
|
|
||||||
super.init()
|
|
||||||
|
|
||||||
self.navigationController = navigationController
|
|
||||||
|
|
||||||
let interactivePushGestureRecognizer = UIScreenEdgePanGestureRecognizer(target: self, action: #selector(handleSwipeForward(_:)))
|
|
||||||
self.interactivePushGestureRecognizer = interactivePushGestureRecognizer
|
|
||||||
|
|
||||||
navigationController.delegate = self
|
|
||||||
interactivePushGestureRecognizer.edges = .right
|
|
||||||
interactivePushGestureRecognizer.require(toFail: navigationController.interactivePopGestureRecognizer!)
|
|
||||||
navigationController.view.addGestureRecognizer(interactivePushGestureRecognizer)
|
|
||||||
|
|
||||||
let trackpadGestureRecognizer = TrackpadScrollGestureRecognizer(target: self, action: #selector(handleSwipeForward(_:)))
|
|
||||||
trackpadGestureRecognizer.require(toFail: navigationController.interactivePopGestureRecognizer!)
|
|
||||||
navigationController.view.addGestureRecognizer(trackpadGestureRecognizer)
|
|
||||||
}
|
|
||||||
|
|
||||||
@objc func handleSwipeForward(_ recognizer: UIPanGestureRecognizer) {
|
|
||||||
interactive = true
|
|
||||||
|
|
||||||
let velocity = recognizer.velocity(in: recognizer.view)
|
|
||||||
let translation = recognizer.translation(in: recognizer.view)
|
|
||||||
let dx = -translation.x / navigationController.view.bounds.width
|
|
||||||
let vx = -velocity.x
|
|
||||||
|
|
||||||
switch recognizer.state {
|
|
||||||
case .began:
|
|
||||||
if let viewController = navigationController.poppedViewControllers.first {
|
|
||||||
pushingViewController = viewController
|
|
||||||
navigationController.poppedViewControllers.removeFirst()
|
|
||||||
navigationController.skipResetPoppedOnNextPush = true
|
|
||||||
navigationController.pushViewController(viewController, animated: true)
|
|
||||||
} else {
|
|
||||||
interactive = false
|
|
||||||
}
|
|
||||||
|
|
||||||
case .changed:
|
|
||||||
update(dx)
|
|
||||||
|
|
||||||
case .ended:
|
|
||||||
if (dx > minimumPushDistanceThreshold || vx > minimumPushVelocityThreshold) {
|
|
||||||
finish()
|
|
||||||
} else {
|
|
||||||
cancel()
|
|
||||||
}
|
|
||||||
interactive = false
|
|
||||||
|
|
||||||
default:
|
|
||||||
cancel()
|
|
||||||
interactive = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
extension InteractivePushTransition: UINavigationControllerDelegate {
|
|
||||||
func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool) {
|
|
||||||
self.navigationController.onWillShow()
|
|
||||||
}
|
|
||||||
|
|
||||||
func navigationController(_ navigationController: UINavigationController, animationControllerFor operation: UINavigationController.Operation, from fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
|
|
||||||
if operation == .push, interactive {
|
|
||||||
return self
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func navigationController(_ navigationController: UINavigationController, interactionControllerFor animationController: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
|
|
||||||
if interactive {
|
|
||||||
return self
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension InteractivePushTransition: UIViewControllerAnimatedTransitioning {
|
|
||||||
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
|
|
||||||
return pushAnimationDuration
|
|
||||||
}
|
|
||||||
|
|
||||||
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
|
|
||||||
let toView = transitionContext.view(forKey: .to)!
|
|
||||||
let fromView = transitionContext.view(forKey: .from)!
|
|
||||||
|
|
||||||
let dimmingView = UIView()
|
|
||||||
dimmingView.backgroundColor = .black
|
|
||||||
dimmingView.alpha = 0
|
|
||||||
dimmingView.frame = fromView.frame
|
|
||||||
|
|
||||||
transitionContext.containerView.addSubview(dimmingView)
|
|
||||||
transitionContext.containerView.addSubview(toView)
|
|
||||||
|
|
||||||
// modify frame of presented view to go from x = <screen width> to x = 0
|
|
||||||
var frame = fromView.frame
|
|
||||||
frame.origin.x = frame.width
|
|
||||||
toView.frame = frame
|
|
||||||
frame.origin.x = 0
|
|
||||||
|
|
||||||
// we want it linear while interactive, but we want the "ease out" animation
|
|
||||||
// if the user flicks the screen ahrd enough to finish the transition without interaction
|
|
||||||
let options = interactive ? UIView.AnimationOptions.curveLinear : .curveEaseOut
|
|
||||||
|
|
||||||
let duration = transitionDuration(using: transitionContext)
|
|
||||||
UIView.animate(withDuration: duration, delay: 0, options: options, animations: {
|
|
||||||
// these magic numbers scientifically determined by repeatedly adjusting and comparing to the system animation
|
|
||||||
let translationDistance = -frame.size.width * 0.3
|
|
||||||
fromView.transform = CGAffineTransform(translationX: translationDistance, y: 0)
|
|
||||||
toView.frame = frame
|
|
||||||
dimmingView.alpha = 0.075
|
|
||||||
}) { (finished) in
|
|
||||||
fromView.transform = .identity
|
|
||||||
dimmingView.removeFromSuperview()
|
|
||||||
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,86 @@
|
||||||
|
//
|
||||||
|
// UIViewController+Children.swift
|
||||||
|
// Gemini-iOS
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 12/17/20.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
// Based on MVCTodo by Dave DeLong: https://github.com/davedelong/MVCTodo/blob/841649dd6aa31bacda3ad7ef9a9a836f66281e50/MVCTodo/Extensions/UIViewController.swift
|
||||||
|
extension UIViewController {
|
||||||
|
func embedChild(_ newChild: UIViewController, in container: UIView? = nil) {
|
||||||
|
// if the view controller is already a child of something else, remove it
|
||||||
|
if let oldParent = newChild.parent, oldParent != self {
|
||||||
|
newChild.beginAppearanceTransition(false, animated: false)
|
||||||
|
newChild.willMove(toParent: nil)
|
||||||
|
newChild.removeFromParent()
|
||||||
|
|
||||||
|
if newChild.viewIfLoaded?.superview != nil {
|
||||||
|
newChild.viewIfLoaded?.removeFromSuperview()
|
||||||
|
}
|
||||||
|
|
||||||
|
newChild.endAppearanceTransition()
|
||||||
|
}
|
||||||
|
|
||||||
|
// since .view returns an IUO, by default the type of this is "UIView?"
|
||||||
|
// explicitly type the variable because We Know Better™
|
||||||
|
var targetContainer: UIView = container ?? self.view
|
||||||
|
if !targetContainer.isContainedWithin(view) {
|
||||||
|
targetContainer = view
|
||||||
|
}
|
||||||
|
|
||||||
|
// add the view controller as a child
|
||||||
|
if newChild.parent != self {
|
||||||
|
newChild.beginAppearanceTransition(true, animated: false)
|
||||||
|
addChild(newChild)
|
||||||
|
newChild.didMove(toParent: self)
|
||||||
|
targetContainer.embedSubview(newChild.view)
|
||||||
|
newChild.endAppearanceTransition()
|
||||||
|
} else {
|
||||||
|
// the view controller is already a child
|
||||||
|
// make sure it's in the right view
|
||||||
|
|
||||||
|
// we don't do the appearance transition stuff here,
|
||||||
|
// because the vc is already a child, so *presumably*
|
||||||
|
// that transition stuff has already appened
|
||||||
|
targetContainer.embedSubview(newChild.view)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func removeViewAndController() {
|
||||||
|
view.removeFromSuperview()
|
||||||
|
removeFromParent()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Based on MVCTodo by Dave DeLong: https://github.com/davedelong/MVCTodo/blob/841649dd6aa31bacda3ad7ef9a9a836f66281e50/MVCTodo/Extensions/UIView.swift
|
||||||
|
extension UIView {
|
||||||
|
func embedSubview(_ subview: UIView) {
|
||||||
|
if subview.superview == self { return }
|
||||||
|
|
||||||
|
if subview.superview != nil {
|
||||||
|
subview.removeFromSuperview()
|
||||||
|
}
|
||||||
|
|
||||||
|
subview.frame = bounds
|
||||||
|
addSubview(subview)
|
||||||
|
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
subview.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||||
|
subview.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||||
|
subview.topAnchor.constraint(equalTo: topAnchor),
|
||||||
|
subview.bottomAnchor.constraint(equalTo: bottomAnchor)
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
func isContainedWithin(_ other: UIView) -> Bool {
|
||||||
|
var current: UIView? = self
|
||||||
|
while let proposedView = current {
|
||||||
|
if proposedView == other { return true }
|
||||||
|
current = proposedView.superview
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -44,10 +44,10 @@
|
||||||
D688F590258AC814003A0A73 /* HTMLEntities in Frameworks */ = {isa = PBXBuildFile; productRef = D688F58F258AC814003A0A73 /* HTMLEntities */; };
|
D688F590258AC814003A0A73 /* HTMLEntities in Frameworks */ = {isa = PBXBuildFile; productRef = D688F58F258AC814003A0A73 /* HTMLEntities */; };
|
||||||
D688F599258ACAAE003A0A73 /* BrowserWebViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D688F598258ACAAE003A0A73 /* BrowserWebViewController.swift */; };
|
D688F599258ACAAE003A0A73 /* BrowserWebViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D688F598258ACAAE003A0A73 /* BrowserWebViewController.swift */; };
|
||||||
D688F5FF258ACE6B003A0A73 /* browser.css in Resources */ = {isa = PBXBuildFile; fileRef = D688F5FE258ACE6B003A0A73 /* browser.css */; };
|
D688F5FF258ACE6B003A0A73 /* browser.css in Resources */ = {isa = PBXBuildFile; fileRef = D688F5FE258ACE6B003A0A73 /* browser.css */; };
|
||||||
D688F621258B0811003A0A73 /* BrowserNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D688F620258B0811003A0A73 /* BrowserNavigationController.swift */; };
|
|
||||||
D688F62A258B0833003A0A73 /* InteractivePushTransition.swift in Sources */ = {isa = PBXBuildFile; fileRef = D688F629258B0833003A0A73 /* InteractivePushTransition.swift */; };
|
|
||||||
D688F633258B09BB003A0A73 /* TrackpadScrollGestureRecognizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D688F632258B09BB003A0A73 /* TrackpadScrollGestureRecognizer.swift */; };
|
D688F633258B09BB003A0A73 /* TrackpadScrollGestureRecognizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D688F632258B09BB003A0A73 /* TrackpadScrollGestureRecognizer.swift */; };
|
||||||
D688F64A258C17F3003A0A73 /* SymbolCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = D688F649258C17F3003A0A73 /* SymbolCache.swift */; };
|
D688F64A258C17F3003A0A73 /* SymbolCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = D688F649258C17F3003A0A73 /* SymbolCache.swift */; };
|
||||||
|
D688F65A258C2256003A0A73 /* BrowserNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D688F659258C2256003A0A73 /* BrowserNavigationController.swift */; };
|
||||||
|
D688F663258C2479003A0A73 /* UIViewController+Children.swift in Sources */ = {isa = PBXBuildFile; fileRef = D688F662258C2479003A0A73 /* UIViewController+Children.swift */; };
|
||||||
D691A64E25217C6F00348C4B /* Preferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = D691A64D25217C6F00348C4B /* Preferences.swift */; };
|
D691A64E25217C6F00348C4B /* Preferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = D691A64D25217C6F00348C4B /* Preferences.swift */; };
|
||||||
D691A66725217FD800348C4B /* PreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D691A66625217FD800348C4B /* PreferencesView.swift */; };
|
D691A66725217FD800348C4B /* PreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D691A66625217FD800348C4B /* PreferencesView.swift */; };
|
||||||
D691A68725223A4700348C4B /* NavigationBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = D691A68625223A4600348C4B /* NavigationBar.swift */; };
|
D691A68725223A4700348C4B /* NavigationBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = D691A68625223A4600348C4B /* NavigationBar.swift */; };
|
||||||
|
@ -309,10 +309,10 @@
|
||||||
D688F585258AC738003A0A73 /* GeminiHTMLRenderer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeminiHTMLRenderer.swift; sourceTree = "<group>"; };
|
D688F585258AC738003A0A73 /* GeminiHTMLRenderer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeminiHTMLRenderer.swift; sourceTree = "<group>"; };
|
||||||
D688F598258ACAAE003A0A73 /* BrowserWebViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowserWebViewController.swift; sourceTree = "<group>"; };
|
D688F598258ACAAE003A0A73 /* BrowserWebViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowserWebViewController.swift; sourceTree = "<group>"; };
|
||||||
D688F5FE258ACE6B003A0A73 /* browser.css */ = {isa = PBXFileReference; lastKnownFileType = text.css; path = browser.css; sourceTree = "<group>"; };
|
D688F5FE258ACE6B003A0A73 /* browser.css */ = {isa = PBXFileReference; lastKnownFileType = text.css; path = browser.css; sourceTree = "<group>"; };
|
||||||
D688F620258B0811003A0A73 /* BrowserNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowserNavigationController.swift; sourceTree = "<group>"; };
|
|
||||||
D688F629258B0833003A0A73 /* InteractivePushTransition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InteractivePushTransition.swift; sourceTree = "<group>"; };
|
|
||||||
D688F632258B09BB003A0A73 /* TrackpadScrollGestureRecognizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrackpadScrollGestureRecognizer.swift; sourceTree = "<group>"; };
|
D688F632258B09BB003A0A73 /* TrackpadScrollGestureRecognizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrackpadScrollGestureRecognizer.swift; sourceTree = "<group>"; };
|
||||||
D688F649258C17F3003A0A73 /* SymbolCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SymbolCache.swift; sourceTree = "<group>"; };
|
D688F649258C17F3003A0A73 /* SymbolCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SymbolCache.swift; sourceTree = "<group>"; };
|
||||||
|
D688F659258C2256003A0A73 /* BrowserNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowserNavigationController.swift; sourceTree = "<group>"; };
|
||||||
|
D688F662258C2479003A0A73 /* UIViewController+Children.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+Children.swift"; sourceTree = "<group>"; };
|
||||||
D691A64D25217C6F00348C4B /* Preferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Preferences.swift; sourceTree = "<group>"; };
|
D691A64D25217C6F00348C4B /* Preferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Preferences.swift; sourceTree = "<group>"; };
|
||||||
D691A66625217FD800348C4B /* PreferencesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesView.swift; sourceTree = "<group>"; };
|
D691A66625217FD800348C4B /* PreferencesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesView.swift; sourceTree = "<group>"; };
|
||||||
D691A6762522382E00348C4B /* BrowserViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowserViewController.swift; sourceTree = "<group>"; };
|
D691A6762522382E00348C4B /* BrowserViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowserViewController.swift; sourceTree = "<group>"; };
|
||||||
|
@ -581,10 +581,10 @@
|
||||||
D6E152A424BFFDF500FDF9D3 /* AppDelegate.swift */,
|
D6E152A424BFFDF500FDF9D3 /* AppDelegate.swift */,
|
||||||
D6E152A624BFFDF500FDF9D3 /* SceneDelegate.swift */,
|
D6E152A624BFFDF500FDF9D3 /* SceneDelegate.swift */,
|
||||||
D688F649258C17F3003A0A73 /* SymbolCache.swift */,
|
D688F649258C17F3003A0A73 /* SymbolCache.swift */,
|
||||||
D688F620258B0811003A0A73 /* BrowserNavigationController.swift */,
|
D688F659258C2256003A0A73 /* BrowserNavigationController.swift */,
|
||||||
D688F629258B0833003A0A73 /* InteractivePushTransition.swift */,
|
|
||||||
D688F632258B09BB003A0A73 /* TrackpadScrollGestureRecognizer.swift */,
|
|
||||||
D688F598258ACAAE003A0A73 /* BrowserWebViewController.swift */,
|
D688F598258ACAAE003A0A73 /* BrowserWebViewController.swift */,
|
||||||
|
D688F632258B09BB003A0A73 /* TrackpadScrollGestureRecognizer.swift */,
|
||||||
|
D688F662258C2479003A0A73 /* UIViewController+Children.swift */,
|
||||||
D691A6762522382E00348C4B /* BrowserViewController.swift */,
|
D691A6762522382E00348C4B /* BrowserViewController.swift */,
|
||||||
D6E152A824BFFDF500FDF9D3 /* ContentView.swift */,
|
D6E152A824BFFDF500FDF9D3 /* ContentView.swift */,
|
||||||
D691A68625223A4600348C4B /* NavigationBar.swift */,
|
D691A68625223A4600348C4B /* NavigationBar.swift */,
|
||||||
|
@ -1103,9 +1103,7 @@
|
||||||
isa = PBXSourcesBuildPhase;
|
isa = PBXSourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
D688F621258B0811003A0A73 /* BrowserNavigationController.swift in Sources */,
|
|
||||||
D691A66725217FD800348C4B /* PreferencesView.swift in Sources */,
|
D691A66725217FD800348C4B /* PreferencesView.swift in Sources */,
|
||||||
D688F62A258B0833003A0A73 /* InteractivePushTransition.swift in Sources */,
|
|
||||||
D688F599258ACAAE003A0A73 /* BrowserWebViewController.swift in Sources */,
|
D688F599258ACAAE003A0A73 /* BrowserWebViewController.swift in Sources */,
|
||||||
D688F633258B09BB003A0A73 /* TrackpadScrollGestureRecognizer.swift in Sources */,
|
D688F633258B09BB003A0A73 /* TrackpadScrollGestureRecognizer.swift in Sources */,
|
||||||
D6E152A524BFFDF500FDF9D3 /* AppDelegate.swift in Sources */,
|
D6E152A524BFFDF500FDF9D3 /* AppDelegate.swift in Sources */,
|
||||||
|
@ -1113,7 +1111,9 @@
|
||||||
D6E152A724BFFDF500FDF9D3 /* SceneDelegate.swift in Sources */,
|
D6E152A724BFFDF500FDF9D3 /* SceneDelegate.swift in Sources */,
|
||||||
D688F64A258C17F3003A0A73 /* SymbolCache.swift in Sources */,
|
D688F64A258C17F3003A0A73 /* SymbolCache.swift in Sources */,
|
||||||
D62BCEE2252553620031D894 /* ActivityView.swift in Sources */,
|
D62BCEE2252553620031D894 /* ActivityView.swift in Sources */,
|
||||||
|
D688F65A258C2256003A0A73 /* BrowserNavigationController.swift in Sources */,
|
||||||
D691A68725223A4700348C4B /* NavigationBar.swift in Sources */,
|
D691A68725223A4700348C4B /* NavigationBar.swift in Sources */,
|
||||||
|
D688F663258C2479003A0A73 /* UIViewController+Children.swift in Sources */,
|
||||||
D691A64E25217C6F00348C4B /* Preferences.swift in Sources */,
|
D691A64E25217C6F00348C4B /* Preferences.swift in Sources */,
|
||||||
D6E152A924BFFDF500FDF9D3 /* ContentView.swift in Sources */,
|
D6E152A924BFFDF500FDF9D3 /* ContentView.swift in Sources */,
|
||||||
);
|
);
|
||||||
|
|
Loading…
Reference in New Issue