Swiftgram/Display/NavigationController.swift
Ilya Laktyushin 7bd11013ea AlertController: fix dismissal by outside tap
NavigationController: fire completion callback after pushViewController
2019-06-11 19:07:50 +02:00

1121 lines
58 KiB
Swift

import Foundation
import UIKit
import AsyncDisplayKit
import SwiftSignalKit
#if BUCK
import DisplayPrivate
#endif
public final class NavigationControllerTheme {
public let navigationBar: NavigationBarTheme
public let emptyAreaColor: UIColor
public let emptyDetailIcon: UIImage?
public init(navigationBar: NavigationBarTheme, emptyAreaColor: UIColor, emptyDetailIcon: UIImage?) {
self.navigationBar = navigationBar
self.emptyAreaColor = emptyAreaColor
self.emptyDetailIcon = emptyDetailIcon
}
}
private final class NavigationControllerContainerView: UIView {
override class var layerClass: AnyClass {
return CATracingLayer.self
}
}
private final class NavigationControllerView: UITracingLayerView {
var inTransition = false
let sharedStatusBar: StatusBar
let containerView: NavigationControllerContainerView
let separatorView: UIView
var navigationBackgroundView: UIView?
var navigationSeparatorView: UIView?
var emptyDetailView: UIImageView?
var topControllerNode: ASDisplayNode?
/*override var accessibilityElements: [Any]? {
get {
var accessibilityElements: [Any] = []
if let topControllerNode = self.topControllerNode {
addAccessibilityChildren(of: topControllerNode, container: self, to: &accessibilityElements)
}
return accessibilityElements
} set(value) {
}
}*/
override init(frame: CGRect) {
self.containerView = NavigationControllerContainerView()
self.separatorView = UIView()
self.sharedStatusBar = StatusBar()
super.init(frame: frame)
self.addSubview(self.containerView)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override class var layerClass: AnyClass {
return CATracingLayer.self
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if self.bounds.contains(point) && self.inTransition {
return self
}
return super.hitTest(point, with: event)
}
}
private enum ControllerTransition {
case none
case appearance
}
private final class ControllerRecord {
let controller: UIViewController
var transition: ControllerTransition = .none
init(controller: UIViewController) {
self.controller = controller
}
}
private enum ControllerLayoutConfiguration {
case single
case masterDetail
}
public enum NavigationControllerMode {
case single
case automaticMasterDetail
}
open class NavigationController: UINavigationController, ContainableController, UIGestureRecognizerDelegate {
public var isOpaqueWhenInOverlay: Bool = true
public var blocksBackgroundWhenInOverlay: Bool = true
public var ready: Promise<Bool> = Promise(true)
public var lockOrientation: Bool = false
public var deferScreenEdgeGestures: UIRectEdge = UIRectEdge()
private let mode: NavigationControllerMode
private var theme: NavigationControllerTheme
public private(set) weak var overlayPresentingController: ViewController?
private var controllerView: NavigationControllerView {
return self.view as! NavigationControllerView
}
private var validLayout: ContainerViewLayout?
private var scheduledLayoutTransitionRequestId: Int = 0
private var scheduledLayoutTransitionRequest: (Int, ContainedViewLayoutTransition)?
private var navigationTransitionCoordinator: NavigationTransitionCoordinator?
private var currentPushDisposable = MetaDisposable()
private var currentPresentDisposable = MetaDisposable()
private var _presentedViewController: UIViewController?
open override var presentedViewController: UIViewController? {
return self._presentedViewController
}
private var _viewControllers: [ControllerRecord] = []
override open var viewControllers: [UIViewController] {
get {
return self._viewControllers.map { $0.controller }
} set(value) {
self.setViewControllers(value, animated: false)
}
}
override open var topViewController: UIViewController? {
return self._viewControllers.last?.controller
}
private var _displayNode: ASDisplayNode?
public var displayNode: ASDisplayNode {
return self._displayNode!
}
public init(mode: NavigationControllerMode, theme: NavigationControllerTheme) {
self.mode = mode
self.theme = theme
super.init(nibName: nil, bundle: nil)
}
public override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
preconditionFailure()
}
public required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.currentPushDisposable.dispose()
self.currentPresentDisposable.dispose()
}
public func combinedSupportedOrientations(currentOrientationToLock: UIInterfaceOrientationMask) -> ViewControllerSupportedOrientations {
var supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .allButUpsideDown)
if let controller = self.viewControllers.last {
if let controller = controller as? ViewController {
if controller.lockOrientation {
if let lockedOrientation = controller.lockedOrientation {
supportedOrientations = supportedOrientations.intersection(ViewControllerSupportedOrientations(regularSize: lockedOrientation, compactSize: lockedOrientation))
} else {
supportedOrientations = supportedOrientations.intersection(ViewControllerSupportedOrientations(regularSize: currentOrientationToLock, compactSize: currentOrientationToLock))
}
} else {
supportedOrientations = supportedOrientations.intersection(controller.supportedOrientations)
}
}
}
return supportedOrientations
}
public func updateTheme(_ theme: NavigationControllerTheme) {
self.theme = theme
if self.isViewLoaded {
self.controllerView.backgroundColor = theme.emptyAreaColor
self.controllerView.separatorView.backgroundColor = theme.navigationBar.separatorColor
self.controllerView.navigationBackgroundView?.backgroundColor = theme.navigationBar.backgroundColor
self.controllerView.navigationSeparatorView?.backgroundColor = theme.navigationBar.separatorColor
if let emptyDetailView = self.controllerView.emptyDetailView {
emptyDetailView.image = theme.emptyDetailIcon
if let image = theme.emptyDetailIcon {
emptyDetailView.frame = CGRect(origin: CGPoint(x: floor(emptyDetailView.center.x - image.size.width / 2.0), y: floor(emptyDetailView.center.y - image.size.height / 2.0)), size: image.size)
}
}
}
}
private var previouslyLaidOutMasterController: UIViewController?
private var previouslyLaidOutTopController: UIViewController?
private func layoutConfiguration(for layout: ContainerViewLayout) -> ControllerLayoutConfiguration {
switch self.mode {
case .single:
return .single
case .automaticMasterDetail:
if case .regular = layout.metrics.widthClass, case .regular = layout.metrics.heightClass {
if layout.size.width > 690.0 {
return .masterDetail
}
}
return .single
}
}
private func layoutDataForConfiguration(_ layoutConfiguration: ControllerLayoutConfiguration, layout: ContainerViewLayout, index: Int) -> (CGRect, ContainerViewLayout) {
switch layoutConfiguration {
case .masterDetail:
let masterWidth: CGFloat = max(320.0, floor(layout.size.width / 3.0))
let detailWidth: CGFloat = layout.size.width - masterWidth
if index == 0 {
return (CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: masterWidth, height: layout.size.height)), ContainerViewLayout(size: CGSize(width: masterWidth, height: layout.size.height), metrics: layout.metrics, intrinsicInsets: layout.intrinsicInsets, safeInsets: layout.safeInsets, statusBarHeight: layout.statusBarHeight, inputHeight: layout.inputHeight, standardInputHeight: layout.standardInputHeight, inputHeightIsInteractivellyChanging: layout.inputHeightIsInteractivellyChanging, inVoiceOver: layout.inVoiceOver))
} else {
let detailFrame = CGRect(origin: CGPoint(x: masterWidth, y: 0.0), size: CGSize(width: detailWidth, height: layout.size.height))
return (CGRect(origin: CGPoint(), size: detailFrame.size), ContainerViewLayout(size: CGSize(width: detailWidth, height: layout.size.height), metrics: LayoutMetrics(widthClass: .regular, heightClass: .regular), intrinsicInsets: layout.intrinsicInsets, safeInsets: layout.safeInsets, statusBarHeight: layout.statusBarHeight, inputHeight: layout.inputHeight, standardInputHeight: layout.standardInputHeight, inputHeightIsInteractivellyChanging: layout.inputHeightIsInteractivellyChanging, inVoiceOver: layout.inVoiceOver))
}
case .single:
return (CGRect(origin: CGPoint(), size: CGSize(width: layout.size.width, height: layout.size.height)), ContainerViewLayout(size: CGSize(width: layout.size.width, height: layout.size.height), metrics: LayoutMetrics(widthClass: .compact, heightClass: .compact), intrinsicInsets: layout.intrinsicInsets, safeInsets: layout.safeInsets, statusBarHeight: layout.statusBarHeight, inputHeight: layout.inputHeight, standardInputHeight: layout.standardInputHeight, inputHeightIsInteractivellyChanging: layout.inputHeightIsInteractivellyChanging, inVoiceOver: layout.inVoiceOver))
}
}
private func updateControllerLayouts(previousControllers: [ControllerRecord], layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
var firstControllerFrameAndLayout: (CGRect, ContainerViewLayout)?
let lastControllerFrameAndLayout: (CGRect, ContainerViewLayout)
let layoutConfiguration = self.layoutConfiguration(for: layout)
switch layoutConfiguration {
case .masterDetail:
self.viewControllers.first?.view.clipsToBounds = true
self.controllerView.containerView.clipsToBounds = true
let masterData = layoutDataForConfiguration(layoutConfiguration, layout: layout, index: 0)
firstControllerFrameAndLayout = masterData
lastControllerFrameAndLayout = layoutDataForConfiguration(layoutConfiguration, layout: layout, index: 1)
if self.controllerView.separatorView.superview == nil {
self.controllerView.addSubview(self.controllerView.separatorView)
}
let navigationBackgroundFrame = CGRect(origin: CGPoint(x: masterData.0.maxX, y: 0.0), size: CGSize(width: lastControllerFrameAndLayout.0.width, height: (layout.statusBarHeight ?? 0.0) + 44.0))
if let navigationBackgroundView = self.controllerView.navigationBackgroundView, let navigationSeparatorView = self.controllerView.navigationSeparatorView, let emptyDetailView = self.controllerView.emptyDetailView {
transition.updateFrame(view: navigationBackgroundView, frame: navigationBackgroundFrame)
transition.updateFrame(view: navigationSeparatorView, frame: CGRect(origin: CGPoint(x: navigationBackgroundFrame.minX, y: navigationBackgroundFrame.maxY), size: CGSize(width: navigationBackgroundFrame.width, height: UIScreenPixel)))
if let image = emptyDetailView.image {
transition.updateFrame(view: emptyDetailView, frame: CGRect(origin: CGPoint(x: masterData.0.maxX + floor((lastControllerFrameAndLayout.0.size.width - image.size.width) / 2.0), y: floor((lastControllerFrameAndLayout.0.size.height - image.size.height) / 2.0)), size: image.size))
}
} else {
let navigationBackgroundView = UIView()
navigationBackgroundView.backgroundColor = self.theme.navigationBar.backgroundColor
let navigationSeparatorView = UIView()
navigationSeparatorView.backgroundColor = self.theme.navigationBar.separatorColor
let emptyDetailView = UIImageView()
emptyDetailView.image = self.theme.emptyDetailIcon
emptyDetailView.alpha = 0.0
self.controllerView.navigationBackgroundView = navigationBackgroundView
self.controllerView.navigationSeparatorView = navigationSeparatorView
self.controllerView.emptyDetailView = emptyDetailView
self.controllerView.insertSubview(navigationBackgroundView, at: 0)
self.controllerView.insertSubview(navigationSeparatorView, at: 1)
self.controllerView.insertSubview(emptyDetailView, at: 2)
navigationBackgroundView.frame = navigationBackgroundFrame
navigationSeparatorView.frame = CGRect(origin: CGPoint(x: navigationBackgroundFrame.minX, y: navigationBackgroundFrame.maxY), size: CGSize(width: navigationBackgroundFrame.width, height: UIScreenPixel))
transition.animatePositionAdditive(layer: navigationBackgroundView.layer, offset: CGPoint(x: navigationBackgroundFrame.width, y: 0.0))
transition.animatePositionAdditive(layer: navigationSeparatorView.layer, offset: CGPoint(x: navigationBackgroundFrame.width, y: 0.0))
if let image = emptyDetailView.image {
emptyDetailView.frame = CGRect(origin: CGPoint(x: masterData.0.maxX + floor((lastControllerFrameAndLayout.0.size.width - image.size.width) / 2.0), y: floor((lastControllerFrameAndLayout.0.size.height - image.size.height) / 2.0)), size: image.size)
}
transition.updateAlpha(layer: emptyDetailView.layer, alpha: 1.0)
}
transition.updateFrame(view: self.controllerView.separatorView, frame: CGRect(origin: CGPoint(x: masterData.0.maxX, y: 0.0), size: CGSize(width: UIScreenPixel, height: layout.size.height)))
case .single:
self.viewControllers.first?.view.clipsToBounds = false
if let navigationBackgroundView = self.controllerView.navigationBackgroundView, let navigationSeparatorView = self.controllerView.navigationSeparatorView {
self.controllerView.navigationBackgroundView = nil
self.controllerView.navigationSeparatorView = nil
transition.updatePosition(layer: navigationBackgroundView.layer, position: CGPoint(x: layout.size.width + navigationBackgroundView.bounds.size.width / 2.0, y: navigationBackgroundView.center.y), completion: { [weak navigationBackgroundView] _ in
navigationBackgroundView?.removeFromSuperview()
})
transition.updatePosition(layer: navigationSeparatorView.layer, position: CGPoint(x: layout.size.width + navigationSeparatorView.bounds.size.width / 2.0, y: navigationSeparatorView.center.y), completion: { [weak navigationSeparatorView] _ in
navigationSeparatorView?.removeFromSuperview()
})
if let emptyDetailView = self.controllerView.emptyDetailView {
self.controllerView.emptyDetailView = nil
transition.updateAlpha(layer: emptyDetailView.layer, alpha: 0.0, completion: { [weak emptyDetailView] _ in
emptyDetailView?.removeFromSuperview()
})
}
}
self.controllerView.containerView.clipsToBounds = false
lastControllerFrameAndLayout = layoutDataForConfiguration(layoutConfiguration, layout: layout, index: 1)
transition.updateFrame(view: self.controllerView.separatorView, frame: CGRect(origin: CGPoint(x: -UIScreenPixel, y: 0.0), size: CGSize(width: UIScreenPixel, height: layout.size.height)), completion: { [weak self] completed in
if let strongSelf = self, completed {
strongSelf.controllerView.separatorView.removeFromSuperview()
}
})
}
transition.updateFrame(view: self.controllerView.containerView, frame: CGRect(origin: CGPoint(x: firstControllerFrameAndLayout?.0.maxX ?? 0.0, y: 0.0), size: lastControllerFrameAndLayout.0.size))
switch layoutConfiguration {
case .single:
if self.controllerView.sharedStatusBar.view.superview != nil {
self.controllerView.sharedStatusBar.removeFromSupernode()
self.controllerView.containerView.layer.setTraceableInfo(nil)
}
case .masterDetail:
if self.controllerView.sharedStatusBar.view.superview == nil {
self.controllerView.addSubnode(self.controllerView.sharedStatusBar)
self.controllerView.containerView.layer.setTraceableInfo(CATracingLayerInfo(shouldBeAdjustedToInverseTransform: true, userData: self, tracingTag: 0, disableChildrenTracingTags: WindowTracingTags.statusBar | WindowTracingTags.keyboard))
}
}
if let _ = layout.statusBarHeight {
self.controllerView.sharedStatusBar.frame = CGRect(origin: CGPoint(), size: CGSize(width: layout.size.width, height: 40.0))
}
var controllersAndFrames: [(Bool, ControllerRecord, ContainerViewLayout)] = []
for i in 0 ..< self._viewControllers.count {
if let controller = self._viewControllers[i].controller as? ViewController {
if i == 0 {
controller.navigationBar?.previousItem = nil
} else if case .masterDetail = layoutConfiguration, i == 1 {
controller.navigationBar?.previousItem = .close
} else {
controller.navigationBar?.previousItem = .item(viewControllers[i - 1].navigationItem)
}
}
viewControllers[i].navigation_setNavigationController(self)
if i == 0, let (_, layout) = firstControllerFrameAndLayout {
controllersAndFrames.append((true, self._viewControllers[i], layout))
} else if i == self._viewControllers.count - 1 {
controllersAndFrames.append((false, self._viewControllers[i], lastControllerFrameAndLayout.1))
}
}
var masterController: UIViewController?
var appearingMasterController: ControllerRecord?
var appearingDetailController: ControllerRecord?
for (isMaster, record, layout) in controllersAndFrames {
let frame: CGRect
if isMaster, let firstControllerFrameAndLayout = firstControllerFrameAndLayout {
masterController = record.controller
frame = firstControllerFrameAndLayout.0
if let controller = masterController as? ViewController {
self.controllerView.sharedStatusBar.statusBarStyle = controller.statusBar.statusBarStyle
}
} else {
frame = lastControllerFrameAndLayout.0
}
let isAppearing = record.controller.view.superview == nil
(record.controller as? ViewController)?.containerLayoutUpdated(layout, transition: isAppearing ? .immediate : transition)
if isAppearing {
if isMaster {
appearingMasterController = record
} else {
appearingDetailController = record
}
} else if record.controller.view.superview !== (isMaster ? self.controllerView : self.controllerView.containerView) {
record.controller.setIgnoreAppearanceMethodInvocations(true)
if isMaster {
self.controllerView.insertSubview(record.controller.view, at: 0)
} else {
self.controllerView.containerView.addSubview(record.controller.view)
}
record.controller.setIgnoreAppearanceMethodInvocations(false)
}
var applyFrame = false
if !isAppearing {
applyFrame = true
} else if case .immediate = transition {
applyFrame = true
}
if applyFrame {
var isPartOfTransition = false
if let navigationTransitionCoordinator = self.navigationTransitionCoordinator {
if navigationTransitionCoordinator.topView == record.controller.view || navigationTransitionCoordinator.bottomView == record.controller.view {
isPartOfTransition = true
}
}
if !isPartOfTransition {
transition.updateFrame(view: record.controller.view, frame: frame)
}
}
}
var animatedAppearingDetailController = false
if let previousController = self.previouslyLaidOutTopController, !controllersAndFrames.contains(where: { $0.1.controller === previousController }), previousController.view.superview != nil {
if transition.isAnimated, let record = appearingDetailController {
animatedAppearingDetailController = true
previousController.viewWillDisappear(true)
record.controller.viewWillAppear(true)
record.controller.setIgnoreAppearanceMethodInvocations(true)
if let controller = record.controller as? ViewController, !controller.hasActiveInput {
let (_, controllerLayout) = self.layoutDataForConfiguration(self.layoutConfiguration(for: layout), layout: layout, index: 1)
let appliedLayout = controllerLayout.withUpdatedInputHeight(controller.hasActiveInput ? controllerLayout.inputHeight : nil)
controller.containerLayoutUpdated(appliedLayout, transition: .immediate)
}
self.controllerView.containerView.addSubview(record.controller.view)
record.controller.setIgnoreAppearanceMethodInvocations(false)
if let _ = previousControllers.index(where: { $0.controller === record.controller }) {
//previousControllers[index].transition = .appearance
let navigationTransitionCoordinator = NavigationTransitionCoordinator(transition: .Pop, container: self.controllerView.containerView, topView: previousController.view, topNavigationBar: (previousController as? ViewController)?.navigationBar, bottomView: record.controller.view, bottomNavigationBar: (record.controller as? ViewController)?.navigationBar)
self.navigationTransitionCoordinator = navigationTransitionCoordinator
self.controllerView.inTransition = true
navigationTransitionCoordinator.animateCompletion(0.0, completion: { [weak self] in
if let strongSelf = self {
strongSelf.navigationTransitionCoordinator = nil
strongSelf.controllerView.inTransition = false
record.controller.viewDidAppear(true)
previousController.setIgnoreAppearanceMethodInvocations(true)
previousController.view.removeFromSuperview()
previousController.setIgnoreAppearanceMethodInvocations(false)
previousController.viewDidDisappear(true)
}
})
} else {
if let index = self._viewControllers.index(where: { $0.controller === previousController }) {
self._viewControllers[index].transition = .appearance
}
let navigationTransitionCoordinator = NavigationTransitionCoordinator(transition: .Push, container: self.controllerView.containerView, topView: record.controller.view, topNavigationBar: (record.controller as? ViewController)?.navigationBar, bottomView: previousController.view, bottomNavigationBar: (previousController as? ViewController)?.navigationBar)
self.navigationTransitionCoordinator = navigationTransitionCoordinator
self.controllerView.inTransition = true
navigationTransitionCoordinator.animateCompletion(0.0, completion: { [weak self] in
if let strongSelf = self {
if let index = strongSelf._viewControllers.index(where: { $0.controller === previousController }) {
strongSelf._viewControllers[index].transition = .none
}
strongSelf.navigationTransitionCoordinator = nil
strongSelf.controllerView.inTransition = false
record.controller.viewDidAppear(true)
previousController.setIgnoreAppearanceMethodInvocations(true)
previousController.view.removeFromSuperview()
previousController.setIgnoreAppearanceMethodInvocations(false)
previousController.viewDidDisappear(true)
}
})
}
} else {
previousController.viewWillDisappear(false)
previousController.view.removeFromSuperview()
previousController.viewDidDisappear(false)
}
}
if !animatedAppearingDetailController, let record = appearingDetailController {
record.controller.viewWillAppear(false)
record.controller.setIgnoreAppearanceMethodInvocations(true)
self.controllerView.containerView.addSubview(record.controller.view)
record.controller.setIgnoreAppearanceMethodInvocations(false)
record.controller.viewDidAppear(false)
if let controller = record.controller as? ViewController {
controller.displayNode.recursivelyEnsureDisplaySynchronously(true)
}
}
if let record = appearingMasterController, let firstControllerFrameAndLayout = firstControllerFrameAndLayout {
record.controller.viewWillAppear(false)
record.controller.setIgnoreAppearanceMethodInvocations(true)
self.controllerView.insertSubview(record.controller.view, belowSubview: self.controllerView.containerView)
record.controller.setIgnoreAppearanceMethodInvocations(false)
record.controller.viewDidAppear(false)
if let controller = record.controller as? ViewController {
controller.displayNode.recursivelyEnsureDisplaySynchronously(true)
}
record.controller.view.frame = firstControllerFrameAndLayout.0
record.controller.viewDidAppear(transition.isAnimated)
transition.animatePositionAdditive(layer: record.controller.view.layer, offset: CGPoint(x: -firstControllerFrameAndLayout.0.width, y: 0.0))
}
for record in self._viewControllers {
let controller = record.controller
if case .none = record.transition, !controllersAndFrames.contains(where: { $0.1.controller === controller }) {
if controller === self.previouslyLaidOutMasterController {
controller.viewWillDisappear(true)
record.transition = .appearance
transition.animatePositionAdditive(layer: controller.view.layer, offset: CGPoint(), to: CGPoint(x: -controller.view.bounds.size.width, y: 0.0), removeOnCompletion: false, completion: { [weak self] in
if let strongSelf = self {
controller.setIgnoreAppearanceMethodInvocations(true)
controller.view.removeFromSuperview()
controller.setIgnoreAppearanceMethodInvocations(false)
controller.viewDidDisappear(true)
controller.view.layer.removeAllAnimations()
for r in strongSelf._viewControllers {
if r.controller === controller {
r.transition = .none
}
}
}
})
} else {
if controller.isViewLoaded && controller.view.superview != nil {
var isPartOfTransition = false
if let navigationTransitionCoordinator = self.navigationTransitionCoordinator {
if navigationTransitionCoordinator.topView == controller.view || navigationTransitionCoordinator.bottomView == controller.view {
isPartOfTransition = true
}
}
if !isPartOfTransition {
controller.viewWillDisappear(false)
controller.setIgnoreAppearanceMethodInvocations(true)
controller.view.removeFromSuperview()
controller.setIgnoreAppearanceMethodInvocations(false)
controller.viewDidDisappear(false)
}
}
}
}
}
for previous in previousControllers {
var isFound = false
inner: for current in self._viewControllers {
if previous.controller === current.controller {
isFound = true
break inner
}
}
if !isFound {
(previous.controller as? ViewController)?.navigationStackConfigurationUpdated(next: [])
}
}
(self.view as! NavigationControllerView).topControllerNode = (self._viewControllers.last?.controller as? ViewController)?.displayNode
for i in 0 ..< self._viewControllers.count {
var currentNext: UIViewController? = (i == (self._viewControllers.count - 1)) ? nil : self._viewControllers[i + 1].controller
if case .single = layoutConfiguration {
currentNext = nil
}
var previousNext: UIViewController?
inner: for j in 0 ..< previousControllers.count {
if previousControllers[j].controller === self._viewControllers[i].controller {
previousNext = (j == (previousControllers.count - 1)) ? nil : previousControllers[j + 1].controller
break inner
}
}
if currentNext !== previousNext {
let next = currentNext as? ViewController
(self._viewControllers[i].controller as? ViewController)?.navigationStackConfigurationUpdated(next: next == nil ? [] : [next!])
}
}
self.previouslyLaidOutMasterController = masterController
self.previouslyLaidOutTopController = self._viewControllers.last?.controller
}
public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
if !self.isViewLoaded {
self.loadView()
}
self.validLayout = layout
transition.updateFrame(view: self.view, frame: CGRect(origin: self.view.frame.origin, size: layout.size))
self.updateControllerLayouts(previousControllers: self._viewControllers, layout: layout, transition: transition)
if let presentedViewController = self.presentedViewController {
let containedLayout = ContainerViewLayout(size: layout.size, metrics: layout.metrics, intrinsicInsets: layout.intrinsicInsets, safeInsets: layout.safeInsets, statusBarHeight: layout.statusBarHeight, inputHeight: layout.inputHeight, standardInputHeight: layout.standardInputHeight, inputHeightIsInteractivellyChanging: layout.inputHeightIsInteractivellyChanging, inVoiceOver: layout.inVoiceOver)
if let presentedViewController = presentedViewController as? ContainableController {
presentedViewController.containerLayoutUpdated(containedLayout, transition: transition)
} else {
transition.updateFrame(view: presentedViewController.view, frame: CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: layout.size.height))
}
}
if let navigationTransitionCoordinator = self.navigationTransitionCoordinator {
navigationTransitionCoordinator.updateProgress()
}
}
public func updateToInterfaceOrientation(_ orientation: UIInterfaceOrientation) {
for record in self._viewControllers {
if let controller = record.controller as? ContainableController {
controller.updateToInterfaceOrientation(orientation)
}
}
}
open override func loadView() {
self._displayNode = ASDisplayNode(viewBlock: {
return NavigationControllerView()
}, didLoad: nil)
self.view = self.displayNode.view
self.view.clipsToBounds = true
self.view.autoresizingMask = []
self.controllerView.backgroundColor = self.theme.emptyAreaColor
self.controllerView.separatorView.backgroundColor = theme.navigationBar.separatorColor
if #available(iOSApplicationExtension 11.0, *) {
self.navigationBar.prefersLargeTitles = false
}
self.navigationBar.removeFromSuperview()
let panRecognizer = InteractiveTransitionGestureRecognizer(target: self, action: #selector(self.panGesture(_:)))
panRecognizer.delegate = self
panRecognizer.delaysTouchesBegan = false
panRecognizer.cancelsTouchesInView = true
self.view.addGestureRecognizer(panRecognizer)
if self.topViewController != nil {
self.topViewController?.view.frame = CGRect(origin: CGPoint(), size: self.view.frame.size)
}
}
@objc func panGesture(_ recognizer: UIPanGestureRecognizer) {
switch recognizer.state {
case UIGestureRecognizerState.began:
guard let layout = self.validLayout else {
return
}
guard self.navigationTransitionCoordinator == nil else {
return
}
let beginGesture: Bool
switch self.layoutConfiguration(for: layout) {
case .masterDetail:
let location = recognizer.location(in: self.controllerView.containerView)
if self.controllerView.containerView.bounds.contains(location) {
beginGesture = self._viewControllers.count >= 3
} else {
beginGesture = false
}
case .single:
beginGesture = self._viewControllers.count >= 2
}
if beginGesture {
let topController = self.viewControllers[self.viewControllers.count - 1] as UIViewController
let bottomController = self.viewControllers[self.viewControllers.count - 2] as UIViewController
if let topController = topController as? ViewController {
if !topController.attemptNavigation({ [weak self] in
let _ = self?.popViewController(animated: true)
}) {
return
}
}
topController.viewWillDisappear(true)
let topView = topController.view!
if let bottomController = bottomController as? ViewController {
let (_, controllerLayout) = self.layoutDataForConfiguration(self.layoutConfiguration(for: layout), layout: layout, index: self.viewControllers.count - 2)
let appliedLayout = controllerLayout.withUpdatedInputHeight(bottomController.hasActiveInput ? controllerLayout.inputHeight : nil)
bottomController.containerLayoutUpdated(appliedLayout, transition: .immediate)
}
bottomController.viewWillAppear(true)
let bottomView = bottomController.view!
if let bottomController = bottomController as? ViewController {
bottomController.displayNode.recursivelyEnsureDisplaySynchronously(true)
}
let navigationTransitionCoordinator = NavigationTransitionCoordinator(transition: .Pop, container: self.controllerView.containerView, topView: topView, topNavigationBar: (topController as? ViewController)?.navigationBar, bottomView: bottomView, bottomNavigationBar: (bottomController as? ViewController)?.navigationBar)
self.navigationTransitionCoordinator = navigationTransitionCoordinator
}
case UIGestureRecognizerState.changed:
if let navigationTransitionCoordinator = self.navigationTransitionCoordinator, !navigationTransitionCoordinator.animatingCompletion {
let translation = recognizer.translation(in: self.view).x
navigationTransitionCoordinator.progress = max(0.0, min(1.0, translation / self.view.frame.width))
}
case UIGestureRecognizerState.ended:
if let navigationTransitionCoordinator = self.navigationTransitionCoordinator, !navigationTransitionCoordinator.animatingCompletion {
let velocity = recognizer.velocity(in: self.view).x
if velocity > 1000 || navigationTransitionCoordinator.progress > 0.2 {
(self.view as! NavigationControllerView).inTransition = true
navigationTransitionCoordinator.animateCompletion(velocity, completion: {
(self.view as! NavigationControllerView).inTransition = false
self.navigationTransitionCoordinator = nil
if self.viewControllers.count >= 2 && self.navigationTransitionCoordinator == nil {
let topController = self.viewControllers[self.viewControllers.count - 1] as UIViewController
let bottomController = self.viewControllers[self.viewControllers.count - 2] as UIViewController
topController.setIgnoreAppearanceMethodInvocations(true)
bottomController.setIgnoreAppearanceMethodInvocations(true)
let _ = self.popViewController(animated: false)
topController.setIgnoreAppearanceMethodInvocations(false)
bottomController.setIgnoreAppearanceMethodInvocations(false)
topController.viewDidDisappear(true)
bottomController.viewDidAppear(true)
}
})
} else {
if self.viewControllers.count >= 2 && self.navigationTransitionCoordinator == nil {
let topController = self.viewControllers[self.viewControllers.count - 1] as UIViewController
let bottomController = self.viewControllers[self.viewControllers.count - 2] as UIViewController
topController.viewWillAppear(true)
bottomController.viewWillDisappear(true)
}
(self.view as! NavigationControllerView).inTransition = true
navigationTransitionCoordinator.animateCancel({
(self.view as! NavigationControllerView).inTransition = false
self.navigationTransitionCoordinator = nil
if self.viewControllers.count >= 2 && self.navigationTransitionCoordinator == nil {
let topController = self.viewControllers[self.viewControllers.count - 1] as UIViewController
let bottomController = self.viewControllers[self.viewControllers.count - 2] as UIViewController
topController.viewDidAppear(true)
bottomController.viewDidDisappear(true)
}
})
}
}
case .cancelled:
if let navigationTransitionCoordinator = self.navigationTransitionCoordinator, !navigationTransitionCoordinator.animatingCompletion {
if self.viewControllers.count >= 2 && self.navigationTransitionCoordinator == nil {
let topController = self.viewControllers[self.viewControllers.count - 1] as UIViewController
let bottomController = self.viewControllers[self.viewControllers.count - 2] as UIViewController
topController.viewWillAppear(true)
bottomController.viewWillDisappear(true)
}
(self.view as! NavigationControllerView).inTransition = true
navigationTransitionCoordinator.animateCancel({
(self.view as! NavigationControllerView).inTransition = false
self.navigationTransitionCoordinator = nil
if self.viewControllers.count >= 2 && self.navigationTransitionCoordinator == nil {
let topController = self.viewControllers[self.viewControllers.count - 1] as UIViewController
let bottomController = self.viewControllers[self.viewControllers.count - 2] as UIViewController
topController.viewDidAppear(true)
bottomController.viewDidDisappear(true)
}
})
}
default:
break
}
}
public func pushViewController(_ controller: ViewController) {
self.pushViewController(controller, completion: {})
}
public func pushViewController(_ controller: ViewController, animated: Bool = true, completion: @escaping () -> Void) {
let navigateAction: () -> Void = { [weak self] in
guard let strongSelf = self else {
return
}
if !controller.hasActiveInput {
strongSelf.view.endEditing(true)
}
strongSelf.scheduleAfterLayout({
guard let strongSelf = self else {
return
}
if let validLayout = strongSelf.validLayout {
let (_, controllerLayout) = strongSelf.layoutDataForConfiguration(strongSelf.layoutConfiguration(for: validLayout), layout: validLayout, index: strongSelf.viewControllers.count)
let appliedLayout = controllerLayout.withUpdatedInputHeight(controller.hasActiveInput ? controllerLayout.inputHeight : nil)
controller.containerLayoutUpdated(appliedLayout, transition: .immediate)
strongSelf.currentPushDisposable.set((controller.ready.get()
|> deliverOnMainQueue
|> take(1)).start(next: { _ in
guard let strongSelf = self else {
return
}
if let validLayout = strongSelf.validLayout {
let (_, controllerLayout) = strongSelf.layoutDataForConfiguration(strongSelf.layoutConfiguration(for: validLayout), layout: validLayout, index: strongSelf.viewControllers.count)
let containerLayout = controllerLayout.withUpdatedInputHeight(controller.hasActiveInput ? controllerLayout.inputHeight : nil)
if containerLayout != appliedLayout {
controller.containerLayoutUpdated(containerLayout, transition: .immediate)
}
strongSelf.pushViewController(controller, animated: animated)
completion()
}
}))
} else {
strongSelf.pushViewController(controller, animated: false)
completion()
}
})
}
if let lastController = self.viewControllers.last as? ViewController, !lastController.attemptNavigation(navigateAction) {
} else {
navigateAction()
}
}
open override func pushViewController(_ viewController: UIViewController, animated: Bool) {
self.currentPushDisposable.set(nil)
var controllers = self.viewControllers
controllers.append(viewController)
self.setViewControllers(controllers, animated: animated)
}
public func replaceTopController(_ controller: ViewController, animated: Bool, ready: ValuePromise<Bool>? = nil) {
self.view.endEditing(true)
if !controller.hasActiveInput {
self.view.endEditing(true)
}
if let validLayout = self.validLayout {
var (_, controllerLayout) = self.layoutDataForConfiguration(self.layoutConfiguration(for: validLayout), layout: validLayout, index: self.viewControllers.count)
controllerLayout.inputHeight = nil
controller.containerLayoutUpdated(controllerLayout, transition: .immediate)
}
self.currentPushDisposable.set((controller.ready.get()
|> deliverOnMainQueue
|> take(1)).start(next: { [weak self] _ in
if let strongSelf = self {
ready?.set(true)
var controllers = strongSelf.viewControllers
controllers.removeLast()
controllers.append(controller)
strongSelf.setViewControllers(controllers, animated: animated)
}
}))
}
public func filterController(_ controller: ViewController, animated: Bool) {
let controllers = self.viewControllers.filter({ $0 !== controller })
if controllers.count != self.viewControllers.count {
self.setViewControllers(controllers, animated: animated)
}
}
public func replaceControllersAndPush(controllers: [UIViewController], controller: ViewController, animated: Bool, ready: ValuePromise<Bool>? = nil, completion: @escaping () -> Void = {}) {
self.view.endEditing(true)
self.scheduleAfterLayout { [weak self] in
guard let strongSelf = self else {
return
}
if let validLayout = strongSelf.validLayout {
var (_, controllerLayout) = strongSelf.layoutDataForConfiguration(strongSelf.layoutConfiguration(for: validLayout), layout: validLayout, index: strongSelf.viewControllers.count)
controllerLayout.inputHeight = nil
controller.containerLayoutUpdated(controllerLayout, transition: .immediate)
}
strongSelf.currentPushDisposable.set((controller.ready.get()
|> deliverOnMainQueue
|> take(1)).start(next: { _ in
guard let strongSelf = self else {
return
}
ready?.set(true)
var controllers = controllers
controllers.append(controller)
strongSelf.setViewControllers(controllers, animated: animated)
completion()
}))
}
}
public func replaceAllButRootController(_ controller: ViewController, animated: Bool, ready: ValuePromise<Bool>? = nil, completion: @escaping () -> Void = {}) {
self.view.endEditing(true)
self.scheduleAfterLayout { [weak self] in
guard let strongSelf = self else {
return
}
if let validLayout = strongSelf.validLayout {
var (_, controllerLayout) = strongSelf.layoutDataForConfiguration(strongSelf.layoutConfiguration(for: validLayout), layout: validLayout, index: strongSelf.viewControllers.count)
controllerLayout.inputHeight = nil
controller.containerLayoutUpdated(controllerLayout, transition: .immediate)
}
strongSelf.currentPushDisposable.set((controller.ready.get()
|> deliverOnMainQueue
|> take(1)).start(next: { _ in
guard let strongSelf = self else {
return
}
ready?.set(true)
var controllers = strongSelf.viewControllers
while controllers.count > 1 {
controllers.removeLast()
}
controllers.append(controller)
strongSelf.setViewControllers(controllers, animated: animated)
completion()
}))
}
}
public func popToRoot(animated: Bool) {
var controllers = self.viewControllers
while controllers.count > 1 {
controllers.removeLast()
}
self.setViewControllers(controllers, animated: animated)
}
override open func popToViewController(_ viewController: UIViewController, animated: Bool) -> [UIViewController]? {
var poppedControllers: [UIViewController] = []
var found = false
var controllers = self.viewControllers
if !controllers.contains(where: { $0 === viewController }) {
return nil
}
while !controllers.isEmpty {
if controllers[controllers.count - 1] === viewController {
found = true
break
}
poppedControllers.insert(controllers[controllers.count - 1], at: 0)
controllers.removeLast()
}
if found {
self.setViewControllers(controllers, animated: animated)
return poppedControllers
} else {
return nil
}
}
open override func popViewController(animated: Bool) -> UIViewController? {
var controller: UIViewController?
var controllers = self.viewControllers
if controllers.count != 0 {
controller = controllers[controllers.count - 1] as UIViewController
controllers.remove(at: controllers.count - 1)
self.setViewControllers(controllers, animated: animated)
}
return controller
}
open override func setViewControllers(_ viewControllers: [UIViewController], animated: Bool) {
var resultControllers: [ControllerRecord] = []
for controller in viewControllers {
var found = false
inner: for current in self._viewControllers {
if current.controller === controller {
resultControllers.append(current)
found = true
break inner
}
}
if !found {
resultControllers.append(ControllerRecord(controller: controller))
}
}
let previousControllers = self._viewControllers
self._viewControllers = resultControllers
if let navigationTransitionCoordinator = self.navigationTransitionCoordinator {
navigationTransitionCoordinator.complete()
}
if let layout = self.validLayout {
self.updateControllerLayouts(previousControllers: previousControllers, layout: layout, transition: animated ? .animated(duration: 0.5, curve: .spring) : .immediate)
}
}
override open func present(_ viewControllerToPresent: UIViewController, animated flag: Bool, completion: (() -> Void)? = nil) {
if let controller = viewControllerToPresent as? NavigationController {
controller.navigation_setDismiss({ [weak self] in
if let strongSelf = self {
strongSelf.dismiss(animated: false, completion: nil)
}
}, rootController: self.view!.window!.rootViewController)
self._presentedViewController = controller
self.view.endEditing(true)
if let validLayout = self.validLayout {
controller.containerLayoutUpdated(validLayout, transition: .immediate)
}
var ready: Signal<Bool, NoError> = .single(true)
if let controller = controller.topViewController as? ViewController {
ready = controller.ready.get()
|> filter { $0 }
|> take(1)
|> deliverOnMainQueue
}
self.currentPresentDisposable.set(ready.start(next: { [weak self] _ in
if let strongSelf = self {
if flag {
controller.view.frame = strongSelf.view.bounds.offsetBy(dx: 0.0, dy: strongSelf.view.bounds.height)
strongSelf.view.addSubview(controller.view)
UIView.animate(withDuration: 0.3, delay: 0.0, options: UIViewAnimationOptions(rawValue: 7 << 16), animations: {
controller.view.frame = strongSelf.view.bounds
}, completion: { _ in
if let completion = completion {
completion()
}
})
} else {
controller.view.frame = strongSelf.view.bounds
strongSelf.view.addSubview(controller.view)
if let completion = completion {
completion()
}
}
}
}))
} else {
preconditionFailure("NavigationController can't present \(viewControllerToPresent). Only subclasses of NavigationController are allowed.")
}
}
override open func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) {
if let controller = self.presentedViewController {
if flag {
UIView.animate(withDuration: 0.3, delay: 0.0, options: UIViewAnimationOptions(rawValue: 7 << 16), animations: {
controller.view.frame = self.view.bounds.offsetBy(dx: 0.0, dy: self.view.bounds.height)
}, completion: { _ in
controller.view.removeFromSuperview()
self._presentedViewController = nil
if let completion = completion {
completion()
}
})
} else {
controller.view.removeFromSuperview()
self._presentedViewController = nil
if let completion = completion {
completion()
}
}
}
}
public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return false
}
public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldBeRequiredToFailBy otherGestureRecognizer: UIGestureRecognizer) -> Bool {
if let _ = otherGestureRecognizer as? UIPanGestureRecognizer {
return true
}
return false
}
public final var currentWindow: WindowHost? {
if let window = self.view.window as? WindowHost {
return window
} else if let superwindow = self.view.window {
for subview in superwindow.subviews {
if let subview = subview as? WindowHost {
return subview
}
}
}
return nil
}
private func scheduleAfterLayout(_ f: @escaping () -> Void) {
(self.view as? UITracingLayerView)?.schedule(layout: {
f()
})
self.view.setNeedsLayout()
}
private func scheduleLayoutTransitionRequest(_ transition: ContainedViewLayoutTransition) {
let requestId = self.scheduledLayoutTransitionRequestId
self.scheduledLayoutTransitionRequestId += 1
self.scheduledLayoutTransitionRequest = (requestId, transition)
(self.view as? UITracingLayerView)?.schedule(layout: { [weak self] in
if let strongSelf = self {
if let (currentRequestId, currentRequestTransition) = strongSelf.scheduledLayoutTransitionRequest, currentRequestId == requestId {
strongSelf.scheduledLayoutTransitionRequest = nil
strongSelf.requestLayout(transition: currentRequestTransition)
}
}
})
self.view.setNeedsLayout()
}
private func requestLayout(transition: ContainedViewLayoutTransition) {
if self.isViewLoaded, let validLayout = self.validLayout {
self.containerLayoutUpdated(validLayout, transition: transition)
}
}
}