Swiftgram/submodules/Display/Source/Navigation/NavigationController.swift
Ilya Laktyushin 022d626000 Various fixes
2024-06-26 13:54:54 +04:00

1884 lines
85 KiB
Swift

import Foundation
import UIKit
import AsyncDisplayKit
import SwiftSignalKit
public enum NavigationStatusBarStyle {
case black
case white
}
public final class NavigationControllerTheme {
public let statusBar: NavigationStatusBarStyle
public let navigationBar: NavigationBarTheme
public let emptyAreaColor: UIColor
public init(statusBar: NavigationStatusBarStyle, navigationBar: NavigationBarTheme, emptyAreaColor: UIColor) {
self.statusBar = statusBar
self.navigationBar = navigationBar
self.emptyAreaColor = emptyAreaColor
}
}
public struct NavigationAnimationOptions : OptionSet {
public let rawValue: Int
public init(rawValue: Int) {
self.rawValue = rawValue
}
public init() {
self.rawValue = 0
}
public static let removeOnMasterDetails = NavigationAnimationOptions(rawValue: 1 << 0)
}
private enum ControllerTransition {
case none
case appearance
}
private final class ControllerRecord {
let controller: UIViewController
var transition: ControllerTransition = .none
init(controller: UIViewController) {
self.controller = controller
}
}
public enum NavigationControllerMode {
case single
case automaticMasterDetail
}
public enum MasterDetailLayoutBlackout : Equatable {
case master
case details
}
private enum RootContainer {
case flat(NavigationContainer)
case split(NavigationSplitContainer)
}
private final class GlobalOverlayContainerParent: ASDisplayNode {
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if let subnodes = self.subnodes {
for node in subnodes.reversed() {
if let result = node.view.hitTest(point, with: event) {
return result
}
}
}
return nil
}
}
private final class NavigationControllerNode: ASDisplayNode {
private weak var controller: NavigationController?
init(controller: NavigationController) {
self.controller = controller
super.init()
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if let controller = self.controller, controller.isInteractionDisabled() {
return self.view
} else {
return super.hitTest(point, with: event)
}
}
override func accessibilityPerformEscape() -> Bool {
if let controller = self.controller, controller.viewControllers.count > 1 {
let _ = self.controller?.popViewController(animated: true)
return true
}
return false
}
}
public protocol NavigationControllerDropContentItem: AnyObject {
}
public final class NavigationControllerDropContent {
public let position: CGPoint
public let item: NavigationControllerDropContentItem
public init(position: CGPoint, item: NavigationControllerDropContentItem) {
self.position = position
self.item = item
}
}
public protocol NavigationDetailsPlaceholderNode: ASDisplayNode {
func updateLayout(size: CGSize, needsTiling: Bool, transition: ContainedViewLayoutTransition)
}
open class NavigationController: UINavigationController, ContainableController, UIGestureRecognizerDelegate {
public var isOpaqueWhenInOverlay: Bool = true
public var blocksBackgroundWhenInOverlay: Bool = true
public var updateTransitionWhenPresentedAsModal: ((CGFloat, ContainedViewLayoutTransition) -> Void)?
private let _ready = Promise<Bool>(true)
open var ready: Promise<Bool> {
return self._ready
}
private var masterDetailsBlackout: MasterDetailLayoutBlackout?
public var lockOrientation: Bool = false
public var deferScreenEdgeGestures: UIRectEdge = UIRectEdge()
public var prefersOnScreenNavigationHidden: Bool {
return (self.topViewController as? ViewController)?.prefersOnScreenNavigationHidden ?? false
}
private let mode: NavigationControllerMode
private var theme: NavigationControllerTheme
private let isFlat: Bool
var inCallNavigate: (() -> Void)?
private var inCallStatusBar: StatusBar?
private var updateInCallStatusBarState: CallStatusBarNode?
private var globalScrollToTopNode: ScrollToTopNode?
private var rootContainer: RootContainer?
private var rootModalFrame: NavigationModalFrame?
private var modalContainers: [NavigationModalContainer] = []
private var overlayContainers: [NavigationOverlayContainer] = []
private var minimizedContainer: MinimizedContainer?
private var globalOverlayContainers: [NavigationOverlayContainer] = []
private var globalOverlayBelowKeyboardContainerParent: GlobalOverlayContainerParent?
private var globalOverlayContainerParent: GlobalOverlayContainerParent?
public var globalOverlayControllersUpdated: (() -> Void)?
public private(set) var validLayout: ContainerViewLayout?
private var validStatusBarStyle: NavigationStatusBarStyle?
private var validStatusBarHidden: Bool?
private var ignoreInputHeight: Bool = false
private var currentStatusBarExternalHidden: Bool = false
private var scheduledLayoutTransitionRequestId: Int = 0
private var scheduledLayoutTransitionRequest: (Int, ContainedViewLayoutTransition)?
private var _presentedViewController: UIViewController?
open override var presentedViewController: UIViewController? {
return self._presentedViewController
}
private var _viewControllers: [ViewController] = []
override open var viewControllers: [UIViewController] {
get {
return self._viewControllers.map { $0 as UIViewController }
} set(value) {
self.setViewControllers(value, animated: false)
}
}
private var _viewControllersPromise = ValuePromise<[UIViewController]>()
public var viewControllersSignal: Signal<[UIViewController], NoError> {
return _viewControllersPromise.get()
}
private var _overlayControllersPromise = ValuePromise<[UIViewController]>()
public var overlayControllersSignal: Signal<[UIViewController], NoError> {
return _overlayControllersPromise.get()
}
override open var topViewController: UIViewController? {
return self._viewControllers.last
}
var topOverlayController: ViewController? {
if let overlayContainer = self.overlayContainers.last {
return overlayContainer.controller
} else {
return nil
}
}
private var _displayNode: ASDisplayNode?
public var displayNode: ASDisplayNode {
if let value = self._displayNode {
return value
}
if !self.isViewLoaded {
self.loadView()
}
return self._displayNode!
}
public var statusBarHost: StatusBarHost? {
didSet {
}
}
var keyboardViewManager: KeyboardViewManager?
var updateSupportedOrientations: (() -> Void)?
public func updateMasterDetailsBlackout(_ blackout: MasterDetailLayoutBlackout?, transition: ContainedViewLayoutTransition) {
self.masterDetailsBlackout = blackout
if isViewLoaded {
self.view.endEditing(true)
}
self.requestLayout(transition: transition)
}
private weak var detailsPlaceholderNode: NavigationDetailsPlaceholderNode?
public func updateDetailsPlaceholderNode(_ node: NavigationDetailsPlaceholderNode?) {
if self.detailsPlaceholderNode !== node {
self.detailsPlaceholderNode?.removeFromSupernode()
self.detailsPlaceholderNode = node
if let node {
self.displayNode.insertSubnode(node, at: 0)
}
}
}
public init(mode: NavigationControllerMode, theme: NavigationControllerTheme, isFlat: Bool = false) {
self.mode = mode
self.theme = theme
self.isFlat = isFlat
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 {
}
public func combinedSupportedOrientations(currentOrientationToLock: UIInterfaceOrientationMask) -> ViewControllerSupportedOrientations {
var supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .allButUpsideDown)
if let rootContainer = self.rootContainer {
switch rootContainer {
case let .flat(container):
supportedOrientations = supportedOrientations.intersection(container.combinedSupportedOrientations(currentOrientationToLock: currentOrientationToLock))
case .split:
break
}
}
for modalContainer in self.modalContainers {
supportedOrientations = supportedOrientations.intersection(modalContainer.container.combinedSupportedOrientations(currentOrientationToLock: currentOrientationToLock))
}
for overlayContrainer in self.overlayContainers {
let controller = overlayContrainer.controller
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)
}
}
for overlayContrainer in self.globalOverlayContainers {
let controller = overlayContrainer.controller
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
}
fileprivate func isInteractionDisabled() -> Bool {
for overlayContainer in self.overlayContainers {
if overlayContainer.blocksInteractionUntilReady && !overlayContainer.isReady {
return true
}
}
if let rootContainer = self.rootContainer {
switch rootContainer {
case let .flat(container):
if container.hasNonReadyControllers() {
return true
}
case let .split(splitContainer):
if splitContainer.hasNonReadyControllers() {
return true
}
}
}
return false
}
public func updateTheme(_ theme: NavigationControllerTheme) {
self.theme = theme
if let rootContainer = self.rootContainer {
switch rootContainer {
case let .split(container):
container.updateTheme(theme: theme)
case .flat:
break
}
}
if self.isViewLoaded {
self.displayNode.backgroundColor = theme.emptyAreaColor
if let layout = self.validLayout {
self.containerLayoutUpdated(layout, transition: .immediate)
}
}
}
open func preferredContentSizeForLayout(_ layout: ContainerViewLayout) -> CGSize? {
return nil
}
open func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
if !self.isViewLoaded {
self.loadView()
}
self.validLayout = layout
self.updateContainers(layout: layout, transition: transition)
}
private weak var currentTopVisibleOverlayContainerStatusBar: NavigationOverlayContainer? = nil
private var isUpdatingContainers: Bool = false
func updateContainersNonReentrant(transition: ContainedViewLayoutTransition) {
if self.isUpdatingContainers {
return
}
if let layout = self.validLayout {
self.updateContainers(layout: layout, transition: transition)
}
}
private func updateContainers(layout rawLayout: ContainerViewLayout, transition: ContainedViewLayoutTransition, completion externalCompletion: (() -> Void)? = nil) {
self.isUpdatingContainers = true
var layout = rawLayout
if self.ignoreInputHeight {
if layout.inputHeight == nil {
self.ignoreInputHeight = false
} else {
layout = layout.withUpdatedInputHeight(nil)
}
}
let initialPrefersOnScreenNavigationHidden = self.collectPrefersOnScreenNavigationHidden()
let belowKeyboardOverlayLayout = layout
var globalOverlayLayout = layout
// globalOverlayLayout.inputHeight = nil
if let globalOverlayBelowKeyboardContainerParent = self.globalOverlayBelowKeyboardContainerParent {
if globalOverlayBelowKeyboardContainerParent.view.superview != self.displayNode.view {
self.displayNode.addSubnode(globalOverlayBelowKeyboardContainerParent)
}
/*overlayLayout.size.height = overlayLayout.size.height - (layout.inputHeight ?? 0.0)
overlayLayout.inputHeight = nil
overlayLayout.inputHeightIsInteractivellyChanging = false*/
}
if let globalOverlayContainerParent = self.globalOverlayContainerParent {
let portraitSize = CGSize(width: min(layout.size.width, layout.size.height), height: max(layout.size.width, layout.size.height))
let screenSize = UIScreen.main.bounds.size
let portraitScreenSize = CGSize(width: min(screenSize.width, screenSize.height), height: max(screenSize.width, screenSize.height))
if portraitSize.width != portraitScreenSize.width || portraitSize.height != portraitScreenSize.height {
if globalOverlayContainerParent.view.superview != self.displayNode.view {
self.displayNode.addSubnode(globalOverlayContainerParent)
}
globalOverlayLayout.size.height = globalOverlayLayout.size.height - (layout.inputHeight ?? 0.0)
globalOverlayLayout.inputHeight = nil
globalOverlayLayout.inputHeightIsInteractivellyChanging = false
} else if layout.inputHeight == nil {
if globalOverlayContainerParent.view.superview != self.displayNode.view {
self.displayNode.addSubnode(globalOverlayContainerParent)
}
} else {
if let statusBarHost = self.statusBarHost, let keyboardWindow = statusBarHost.keyboardWindow, let keyboardView = statusBarHost.keyboardView, !keyboardView.frame.height.isZero, isViewVisibleInHierarchy(keyboardView) {
if globalOverlayContainerParent.view.superview != keyboardWindow {
globalOverlayContainerParent.layer.zPosition = 1000.0
keyboardWindow.addSubnode(globalOverlayContainerParent)
}
} else if globalOverlayContainerParent.view.superview !== self.displayNode.view {
self.displayNode.addSubnode(globalOverlayContainerParent)
}
}
}
if let globalScrollToTopNode = self.globalScrollToTopNode {
globalScrollToTopNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -1.0), size: CGSize(width: layout.size.width, height: 1.0))
}
let overlayContainerLayout = layout
if let inCallStatusBar = self.inCallStatusBar {
let isLandscape = layout.size.width > layout.size.height
var minHeight: CGFloat
if case .compact = layout.metrics.widthClass, isLandscape {
minHeight = 22.0
} else {
minHeight = 40.0
}
var inCallStatusBarFrame = CGRect(origin: CGPoint(), size: CGSize(width: layout.size.width, height: max(layout.statusBarHeight ?? 0.0, max(minHeight, layout.safeInsets.top))))
if !isLandscape {
if layout.deviceMetrics.hasTopNotch {
inCallStatusBarFrame.size.height += 12.0
} else if layout.deviceMetrics.hasDynamicIsland {
inCallStatusBarFrame.size.height += 20.0
}
}
if inCallStatusBar.frame.isEmpty {
inCallStatusBar.frame = inCallStatusBarFrame
} else {
transition.updateFrame(node: inCallStatusBar, frame: inCallStatusBarFrame)
}
inCallStatusBar.callStatusBarNode?.update(size: inCallStatusBarFrame.size)
inCallStatusBar.callStatusBarNode?.frame = inCallStatusBarFrame
layout.statusBarHeight = inCallStatusBarFrame.height
inCallStatusBar.frame = inCallStatusBarFrame
if let forceInCallStatusBar = self.updateInCallStatusBarState {
self.updateInCallStatusBarState = nil
inCallStatusBar.updateState(statusBar: nil, withSafeInsets: !layout.safeInsets.top.isZero, inCallNode: forceInCallStatusBar, animated: false)
}
}
if let globalOverlayBelowKeyboardContainerParent = self.globalOverlayBelowKeyboardContainerParent {
transition.updateFrame(node: globalOverlayBelowKeyboardContainerParent, frame: CGRect(origin: CGPoint(), size: layout.size))
}
if let globalOverlayContainerParent = self.globalOverlayContainerParent {
transition.updateFrame(node: globalOverlayContainerParent, frame: CGRect(origin: CGPoint(), size: layout.size))
}
let navigationLayout = makeNavigationLayout(mode: self.mode, layout: layout, controllers: self._viewControllers)
var transition = transition
var statusBarStyle: StatusBarStyle = .Ignore
var statusBarHidden = false
var animateStatusBarStyleTransition = transition.isAnimated
var modalContainers: [NavigationModalContainer] = []
for i in 0 ..< navigationLayout.modal.count {
var existingModalContainer: NavigationModalContainer?
loop: for currentModalContainer in self.modalContainers {
for controller in navigationLayout.modal[i].controllers {
if currentModalContainer.container.controllers.contains(where: { $0 === controller }) {
existingModalContainer = currentModalContainer
break loop
}
}
}
let modalContainer: NavigationModalContainer
if let existingModalContainer = existingModalContainer {
modalContainer = existingModalContainer
} else {
modalContainer = NavigationModalContainer(theme: self.theme, isFlat: navigationLayout.modal[i].isFlat, controllerRemoved: { [weak self] controller in
self?.controllerRemoved(controller)
})
modalContainer.container.statusBarStyleUpdated = { [weak self] transition in
guard let strongSelf = self else {
return
}
strongSelf.updateContainersNonReentrant(transition: transition)
}
self.modalContainers.append(modalContainer)
if !modalContainer.isReady {
modalContainer.isReadyUpdated = { [weak self, weak modalContainer] in
guard let strongSelf = self, let _ = modalContainer else {
return
}
strongSelf.updateContainersNonReentrant(transition: .animated(duration: 0.5, curve: .spring))
}
}
modalContainer.updateDismissProgress = { [weak self, weak modalContainer] _, transition in
guard let strongSelf = self, let _ = modalContainer else {
return
}
strongSelf.updateContainersNonReentrant(transition: transition)
}
modalContainer.interactivelyDismissed = { [weak self, weak modalContainer] hadInputFocus in
guard let strongSelf = self, let modalContainer = modalContainer else {
return
}
let controllers = strongSelf._viewControllers.filter { controller in
return !modalContainer.container.controllers.contains(where: { $0 === controller })
}
strongSelf.ignoreInputHeight = hadInputFocus
strongSelf.setViewControllers(controllers, animated: false)
strongSelf.ignoreInputHeight = false
}
}
modalContainers.append(modalContainer)
}
for container in self.modalContainers {
if !modalContainers.contains(where: { $0 === container }) {
if viewTreeContainsFirstResponder(view: container.view) {
self.ignoreInputHeight = true
container.view.endEditing(true)
}
transition = container.dismiss(transition: transition, completion: { [weak container] in
container?.removeFromSupernode()
})
}
}
self.modalContainers = modalContainers
var topVisibleOverlayContainerWithStatusBar: NavigationOverlayContainer?
var notifyGlobalOverlayControllersUpdated = false
var additionalSideInsets = UIEdgeInsets()
var modalStyleOverlayTransitionFactor: CGFloat = 0.0
var previousGlobalOverlayBelowKeyboardContainer: NavigationOverlayContainer?
var previousGlobalOverlayContainer: NavigationOverlayContainer?
for i in (0 ..< self.globalOverlayContainers.count).reversed() {
let overlayContainer = self.globalOverlayContainers[i]
let containerTransition: ContainedViewLayoutTransition
if overlayContainer.supernode == nil {
containerTransition = .immediate
} else {
containerTransition = transition
}
let overlayWantsToBeBelowKeyboard = overlayContainer.controller.overlayWantsToBeBelowKeyboard
let overlayLayout: ContainerViewLayout
if overlayWantsToBeBelowKeyboard {
overlayLayout = belowKeyboardOverlayLayout
} else {
overlayLayout = globalOverlayLayout
}
containerTransition.updateFrame(node: overlayContainer, frame: CGRect(origin: CGPoint(), size: overlayLayout.size))
overlayContainer.update(layout: overlayLayout, transition: containerTransition)
modalStyleOverlayTransitionFactor = max(modalStyleOverlayTransitionFactor, overlayContainer.controller.modalStyleOverlayTransitionFactor)
if overlayContainer.isReady && !overlayContainer.isRemoved {
let wasNotAdded = overlayContainer.supernode == nil
if overlayWantsToBeBelowKeyboard {
if overlayContainer.supernode !== self.globalOverlayBelowKeyboardContainerParent {
if let previousGlobalOverlayBelowKeyboardContainer = previousGlobalOverlayBelowKeyboardContainer {
self.globalOverlayBelowKeyboardContainerParent?.insertSubnode(overlayContainer, belowSubnode: previousGlobalOverlayBelowKeyboardContainer)
} else {
self.globalOverlayBelowKeyboardContainerParent?.addSubnode(overlayContainer)
}
}
} else {
if overlayContainer.supernode !== self.globalOverlayContainerParent {
if let previousGlobalOverlayContainer = previousGlobalOverlayContainer {
self.globalOverlayContainerParent?.insertSubnode(overlayContainer, belowSubnode: previousGlobalOverlayContainer)
} else {
self.globalOverlayContainerParent?.addSubnode(overlayContainer)
}
}
}
if wasNotAdded {
overlayContainer.transitionIn()
notifyGlobalOverlayControllersUpdated = true
overlayContainer.controller.internalOverlayWantsToBeBelowKeyboardUpdated = { [weak self] transition in
guard let strongSelf = self else {
return
}
strongSelf.updateContainersNonReentrant(transition: transition)
}
}
}
let controllerAdditionalSideInsets = overlayContainer.controller.additionalSideInsets
additionalSideInsets = UIEdgeInsets(top: 0.0, left: max(additionalSideInsets.left, controllerAdditionalSideInsets.left), bottom: 0.0, right: max(additionalSideInsets.right, controllerAdditionalSideInsets.right))
if overlayContainer.supernode != nil {
if overlayContainer.controller.overlayWantsToBeBelowKeyboard {
previousGlobalOverlayBelowKeyboardContainer = overlayContainer
} else {
previousGlobalOverlayContainer = overlayContainer
}
let controllerStatusBarStyle = overlayContainer.controller.statusBar.statusBarStyle
switch controllerStatusBarStyle {
case .Black, .White, .Hide:
if topVisibleOverlayContainerWithStatusBar == nil {
topVisibleOverlayContainerWithStatusBar = overlayContainer
}
if case .Hide = controllerStatusBarStyle {
statusBarHidden = true
} else {
statusBarHidden = overlayContainer.controller.statusBar.alpha.isZero
}
case .Ignore:
break
}
}
}
var previousOverlayContainer: NavigationOverlayContainer?
for i in (0 ..< self.overlayContainers.count).reversed() {
let overlayContainer = self.overlayContainers[i]
let containerTransition: ContainedViewLayoutTransition
if overlayContainer.supernode == nil {
containerTransition = .immediate
} else {
containerTransition = transition
}
containerTransition.updateFrame(node: overlayContainer, frame: CGRect(origin: CGPoint(), size: overlayContainerLayout.size))
overlayContainer.update(layout: overlayContainerLayout, transition: containerTransition)
modalStyleOverlayTransitionFactor = max(modalStyleOverlayTransitionFactor, overlayContainer.controller.modalStyleOverlayTransitionFactor)
if overlayContainer.supernode == nil && overlayContainer.isReady {
if let previousOverlayContainer = previousOverlayContainer {
self.displayNode.insertSubnode(overlayContainer, belowSubnode: previousOverlayContainer)
} else if let globalScrollToTopNode = self.globalScrollToTopNode {
self.displayNode.insertSubnode(overlayContainer, belowSubnode: globalScrollToTopNode)
} else if let globalOverlayBelowKeyboardContainerParent = self.globalOverlayBelowKeyboardContainerParent {
self.displayNode.insertSubnode(overlayContainer, belowSubnode: globalOverlayBelowKeyboardContainerParent)
} else if let globalOverlayContainerParent = self.globalOverlayContainerParent {
self.displayNode.insertSubnode(overlayContainer, belowSubnode: globalOverlayContainerParent)
} else {
self.displayNode.addSubnode(overlayContainer)
}
overlayContainer.transitionIn()
}
let controllerAdditionalSideInsets = overlayContainer.controller.additionalSideInsets
additionalSideInsets = UIEdgeInsets(top: 0.0, left: max(additionalSideInsets.left, controllerAdditionalSideInsets.left), bottom: 0.0, right: max(additionalSideInsets.right, controllerAdditionalSideInsets.right))
if overlayContainer.supernode != nil {
previousOverlayContainer = overlayContainer
let controllerStatusBarStyle = overlayContainer.controller.statusBar.statusBarStyle
switch controllerStatusBarStyle {
case .Black, .White, .Hide:
if topVisibleOverlayContainerWithStatusBar == nil {
topVisibleOverlayContainerWithStatusBar = overlayContainer
}
if case .Hide = controllerStatusBarStyle {
statusBarHidden = true
} else {
statusBarHidden = overlayContainer.controller.statusBar.alpha.isZero
}
case .Ignore:
break
}
}
}
if self.currentTopVisibleOverlayContainerStatusBar !== topVisibleOverlayContainerWithStatusBar {
animateStatusBarStyleTransition = true
self.currentTopVisibleOverlayContainerStatusBar = topVisibleOverlayContainerWithStatusBar
}
var previousModalContainer: NavigationModalContainer?
var topVisibleModalContainerWithStatusBar: NavigationModalContainer?
var visibleModalCount = 0
var topModalIsFlat = false
var topFlatModalHasProgress = false
let isLandscape = layout.orientation == .landscape
var hasVisibleStandaloneModal = false
var topModalDismissProgress: CGFloat = 0.0
for i in (0 ..< navigationLayout.modal.count).reversed() {
let modalContainer = self.modalContainers[i]
var isStandaloneModal = false
if let controller = modalContainer.container.controllers.first, case .standaloneModal = controller.navigationPresentation {
isStandaloneModal = true
}
let containerTransition: ContainedViewLayoutTransition
if modalContainer.supernode == nil {
containerTransition = .immediate
} else {
containerTransition = transition
}
let effectiveModalTransition: CGFloat
if visibleModalCount == 0 || navigationLayout.modal[i].isFlat {
effectiveModalTransition = 0.0
} else if visibleModalCount == 1 {
effectiveModalTransition = 1.0 - topModalDismissProgress
} else {
effectiveModalTransition = 1.0
}
if navigationLayout.modal[i].isFlat, let lastController = navigationLayout.modal[i].controllers.last {
lastController.modalStyleOverlayTransitionFactorUpdated = { [weak self] transition in
guard let strongSelf = self else {
return
}
strongSelf.updateContainersNonReentrant(transition: transition)
}
modalStyleOverlayTransitionFactor = max(modalStyleOverlayTransitionFactor, lastController.modalStyleOverlayTransitionFactor)
topFlatModalHasProgress = modalStyleOverlayTransitionFactor > 0.0
}
containerTransition.updateFrame(node: modalContainer, frame: CGRect(origin: CGPoint(), size: layout.size))
modalContainer.update(layout: modalContainer.isFlat ? globalOverlayLayout : layout, controllers: navigationLayout.modal[i].controllers, coveredByModalTransition: effectiveModalTransition, transition: containerTransition)
if modalContainer.supernode == nil && modalContainer.isReady {
if let previousModalContainer = previousModalContainer {
self.displayNode.insertSubnode(modalContainer, belowSubnode: previousModalContainer)
} else if let inCallStatusBar = self.inCallStatusBar {
self.displayNode.insertSubnode(modalContainer, belowSubnode: inCallStatusBar)
} else if let previousOverlayContainer = previousOverlayContainer {
self.displayNode.insertSubnode(modalContainer, belowSubnode: previousOverlayContainer)
} else if let globalScrollToTopNode = self.globalScrollToTopNode {
self.displayNode.insertSubnode(modalContainer, belowSubnode: globalScrollToTopNode)
} else {
self.displayNode.addSubnode(modalContainer)
}
modalContainer.animateIn(transition: transition)
}
if modalContainer.supernode != nil {
if !hasVisibleStandaloneModal && !isStandaloneModal && !modalContainer.isFlat {
visibleModalCount += 1
}
if isStandaloneModal {
hasVisibleStandaloneModal = true
visibleModalCount = 0
}
if previousModalContainer == nil {
topModalIsFlat = modalContainer.isFlat
topModalDismissProgress = modalContainer.dismissProgress
if case .compact = layout.metrics.widthClass {
modalContainer.keyboardViewManager = self.keyboardViewManager
modalContainer.canHaveKeyboardFocus = true
} else {
modalContainer.keyboardViewManager = nil
modalContainer.canHaveKeyboardFocus = true
}
if modalContainer.isFlat {
let controllerStatusBarStyle = modalContainer.container.statusBarStyle
switch controllerStatusBarStyle {
case .Black, .White, .Hide:
if topVisibleModalContainerWithStatusBar == nil {
topVisibleModalContainerWithStatusBar = modalContainer
}
if case .Hide = controllerStatusBarStyle {
statusBarHidden = true
} else {
statusBarHidden = false
}
case .Ignore:
break
}
}
} else {
modalContainer.keyboardViewManager = nil
modalContainer.canHaveKeyboardFocus = false
if modalContainer.isFlat {
let controllerStatusBarStyle = modalContainer.container.statusBarStyle
switch controllerStatusBarStyle {
case .Black, .White, .Hide:
if topVisibleModalContainerWithStatusBar == nil {
topVisibleModalContainerWithStatusBar = modalContainer
}
case .Ignore:
break
}
}
}
previousModalContainer = modalContainer
if isStandaloneModal {
switch modalContainer.container.statusBarStyle {
case .Hide:
statusBarHidden = true
default:
break
}
}
}
}
if self.isMaximizing && layout.size.width < layout.size.height {
modalStyleOverlayTransitionFactor = 1.0
topFlatModalHasProgress = true
}
layout.additionalInsets.left = max(layout.intrinsicInsets.left, additionalSideInsets.left)
layout.additionalInsets.right = max(layout.intrinsicInsets.right, additionalSideInsets.right)
switch navigationLayout.root {
case let .flat(controllers):
if let rootContainer = self.rootContainer {
switch rootContainer {
case let .flat(flatContainer):
if previousModalContainer == nil {
flatContainer.keyboardViewManager = self.keyboardViewManager
flatContainer.canHaveKeyboardFocus = true
} else {
flatContainer.keyboardViewManager = nil
flatContainer.canHaveKeyboardFocus = false
}
var updatedSize = layout.size
var updatedIntrinsicInsets = layout.intrinsicInsets
if let minimizedContainer = self.minimizedContainer, (layout.inputHeight ?? 0.0).isZero {
updatedSize.height -= minimizedContainer.collapsedHeight(layout: layout)
updatedIntrinsicInsets.bottom = 0.0
}
let updatedLayout = layout.withUpdatedSize(updatedSize).withUpdatedIntrinsicInsets(updatedIntrinsicInsets)
transition.updateFrame(node: flatContainer, frame: CGRect(origin: CGPoint(), size: updatedSize))
flatContainer.update(layout: updatedLayout, canBeClosed: false, controllers: controllers, transition: transition)
case let .split(splitContainer):
let flatContainer = NavigationContainer(isFlat: self.isFlat, controllerRemoved: { [weak self] controller in
self?.controllerRemoved(controller)
})
flatContainer.requestFilterController = { [weak self] controller in
self?.filterController(controller, animated: true)
}
flatContainer.statusBarStyleUpdated = { [weak self] transition in
guard let strongSelf = self else {
return
}
strongSelf.updateContainersNonReentrant(transition: transition)
}
if previousModalContainer == nil {
flatContainer.keyboardViewManager = self.keyboardViewManager
flatContainer.canHaveKeyboardFocus = true
} else {
flatContainer.keyboardViewManager = nil
flatContainer.canHaveKeyboardFocus = false
}
if let detailsPlaceholderNode = self.detailsPlaceholderNode {
self.displayNode.insertSubnode(flatContainer, aboveSubnode: detailsPlaceholderNode)
} else {
self.displayNode.insertSubnode(flatContainer, at: 0)
}
self.rootContainer = .flat(flatContainer)
flatContainer.frame = CGRect(origin: CGPoint(), size: layout.size)
flatContainer.update(layout: layout, canBeClosed: false, controllers: controllers, transition: .immediate)
splitContainer.removeFromSupernode()
}
} else {
let flatContainer = NavigationContainer(isFlat: self.isFlat, controllerRemoved: { [weak self] controller in
self?.controllerRemoved(controller)
})
flatContainer.requestFilterController = { [weak self] controller in
self?.filterController(controller, animated: true)
}
flatContainer.statusBarStyleUpdated = { [weak self] transition in
guard let strongSelf = self else {
return
}
strongSelf.updateContainersNonReentrant(transition: transition)
}
if previousModalContainer == nil {
flatContainer.keyboardViewManager = self.keyboardViewManager
flatContainer.canHaveKeyboardFocus = true
} else {
flatContainer.keyboardViewManager = nil
flatContainer.canHaveKeyboardFocus = false
}
if let detailsPlaceholderNode = self.detailsPlaceholderNode {
self.displayNode.insertSubnode(flatContainer, aboveSubnode: detailsPlaceholderNode)
} else {
self.displayNode.insertSubnode(flatContainer, at: 0)
}
self.rootContainer = .flat(flatContainer)
flatContainer.frame = CGRect(origin: CGPoint(), size: layout.size)
flatContainer.update(layout: layout, canBeClosed: false, controllers: controllers, transition: .immediate)
}
case let .split(masterControllers, detailControllers):
if let rootContainer = self.rootContainer {
switch rootContainer {
case let .flat(flatContainer):
let splitContainer = NavigationSplitContainer(theme: self.theme, controllerRemoved: { [weak self] controller in
self?.controllerRemoved(controller)
}, scrollToTop: { [weak self] subject in
self?.scrollToTop(subject)
})
if let detailsPlaceholderNode = self.detailsPlaceholderNode {
self.displayNode.insertSubnode(splitContainer, aboveSubnode: detailsPlaceholderNode)
} else {
self.displayNode.insertSubnode(splitContainer, at: 0)
}
self.rootContainer = .split(splitContainer)
if previousModalContainer == nil {
splitContainer.canHaveKeyboardFocus = true
} else {
splitContainer.canHaveKeyboardFocus = false
}
splitContainer.frame = CGRect(origin: CGPoint(), size: layout.size)
splitContainer.update(layout: layout, masterControllers: masterControllers, detailControllers: detailControllers, detailsPlaceholderNode: self.detailsPlaceholderNode, transition: .immediate)
flatContainer.statusBarStyleUpdated = nil
flatContainer.removeFromSupernode()
case let .split(splitContainer):
if previousModalContainer == nil {
splitContainer.canHaveKeyboardFocus = true
} else {
splitContainer.canHaveKeyboardFocus = false
}
transition.updateFrame(node: splitContainer, frame: CGRect(origin: CGPoint(), size: layout.size))
splitContainer.update(layout: layout, masterControllers: masterControllers, detailControllers: detailControllers, detailsPlaceholderNode: self.detailsPlaceholderNode, transition: transition)
}
} else {
let splitContainer = NavigationSplitContainer(theme: self.theme, controllerRemoved: { [weak self] controller in
self?.controllerRemoved(controller)
}, scrollToTop: { [weak self] subject in
self?.scrollToTop(subject)
})
if let detailsPlaceholderNode = self.detailsPlaceholderNode {
self.displayNode.insertSubnode(splitContainer, aboveSubnode: detailsPlaceholderNode)
} else {
self.displayNode.insertSubnode(splitContainer, at: 0)
}
self.rootContainer = .split(splitContainer)
if previousModalContainer == nil {
splitContainer.canHaveKeyboardFocus = true
} else {
splitContainer.canHaveKeyboardFocus = false
}
splitContainer.frame = CGRect(origin: CGPoint(), size: layout.size)
splitContainer.update(layout: layout, masterControllers: masterControllers, detailControllers: detailControllers, detailsPlaceholderNode: self.detailsPlaceholderNode, transition: .immediate)
}
}
if let rootContainer = self.rootContainer {
switch rootContainer {
case let .flat(container):
statusBarStyle = container.statusBarStyle
self.globalScrollToTopNode?.isHidden = false
case .split:
self.globalScrollToTopNode?.isHidden = true
}
}
if self._keepModalDismissProgress {
modalStyleOverlayTransitionFactor = 0.0
self._keepModalDismissProgress = false
}
topModalDismissProgress = max(topModalDismissProgress, modalStyleOverlayTransitionFactor)
switch layout.metrics.widthClass {
case .compact:
if visibleModalCount != 0 || !modalStyleOverlayTransitionFactor.isZero {
let effectiveRootModalDismissProgress: CGFloat
let visibleRootModalDismissProgress: CGFloat
var additionalModalFrameProgress: CGFloat
if visibleModalCount == 1 {
if topFlatModalHasProgress {
effectiveRootModalDismissProgress = 0.0
visibleRootModalDismissProgress = effectiveRootModalDismissProgress
additionalModalFrameProgress = 1.0 - topModalDismissProgress
} else {
effectiveRootModalDismissProgress = topModalDismissProgress
visibleRootModalDismissProgress = effectiveRootModalDismissProgress
additionalModalFrameProgress = 0.0
}
} else if visibleModalCount >= 2 {
effectiveRootModalDismissProgress = 0.0
visibleRootModalDismissProgress = topModalDismissProgress
additionalModalFrameProgress = 1.0 - topModalDismissProgress
} else {
effectiveRootModalDismissProgress = 1.0 - modalStyleOverlayTransitionFactor
visibleRootModalDismissProgress = effectiveRootModalDismissProgress
if visibleModalCount == 0 {
additionalModalFrameProgress = 0.0
} else {
additionalModalFrameProgress = 1.0
}
}
let rootModalFrame: NavigationModalFrame
let modalFrameTransition: ContainedViewLayoutTransition = transition
var forceStatusBarAnimation = false
if let current = self.rootModalFrame {
rootModalFrame = current
transition.updateFrame(node: rootModalFrame, frame: CGRect(origin: CGPoint(), size: layout.size))
rootModalFrame.update(layout: layout, transition: modalFrameTransition)
rootModalFrame.updateDismissal(transition: transition, progress: effectiveRootModalDismissProgress, additionalProgress: additionalModalFrameProgress, completion: {})
forceStatusBarAnimation = true
} else {
rootModalFrame = NavigationModalFrame()
self.rootModalFrame = rootModalFrame
if let rootContainer = self.rootContainer {
var rootContainerNode: ASDisplayNode
switch rootContainer {
case let .flat(container):
rootContainerNode = container
case let .split(container):
rootContainerNode = container
}
self.displayNode.insertSubnode(rootModalFrame, aboveSubnode: rootContainerNode)
}
rootModalFrame.frame = CGRect(origin: CGPoint(), size: layout.size)
rootModalFrame.update(layout: layout, transition: .immediate)
rootModalFrame.updateDismissal(transition: transition, progress: effectiveRootModalDismissProgress, additionalProgress: additionalModalFrameProgress, completion: {})
}
if effectiveRootModalDismissProgress < 0.5 {
statusBarStyle = .White
if forceStatusBarAnimation {
animateStatusBarStyleTransition = true
}
} else {
if forceStatusBarAnimation {
animateStatusBarStyleTransition = true
}
}
if let rootContainer = self.rootContainer {
var rootContainerNode: ASDisplayNode
switch rootContainer {
case let .flat(container):
rootContainerNode = container
case let .split(container):
rootContainerNode = container
}
var topInset: CGFloat = 0.0
if let statusBarHeight = layout.statusBarHeight {
topInset += statusBarHeight
}
let maxScale: CGFloat
let maxOffset: CGFloat
if (topModalIsFlat && !topFlatModalHasProgress) || isLandscape {
maxScale = 1.0
maxOffset = 0.0
} else if visibleModalCount <= 1 {
maxScale = (layout.size.width - 16.0 * 2.0) / layout.size.width
maxOffset = (topInset - (layout.size.height - layout.size.height * maxScale) / 2.0)
} else {
maxScale = (layout.size.width - 16.0 * 2.0 * 2.0) / layout.size.width
maxOffset = (topInset + 10.0 - (layout.size.height - layout.size.height * maxScale) / 2.0)
}
let scale = 1.0 * visibleRootModalDismissProgress + (1.0 - visibleRootModalDismissProgress) * maxScale
let offset = (1.0 - visibleRootModalDismissProgress) * maxOffset
transition.updateSublayerTransformScaleAndOffset(node: rootContainerNode, scale: scale, offset: CGPoint(x: 0.0, y: offset), beginWithCurrentState: true)
}
} else {
if let rootModalFrame = self.rootModalFrame {
self.rootModalFrame = nil
rootModalFrame.updateDismissal(transition: transition, progress: 1.0, additionalProgress: 0.0, completion: { [weak rootModalFrame] in
rootModalFrame?.removeFromSupernode()
})
}
if let rootContainer = self.rootContainer {
var rootContainerNode: ASDisplayNode
switch rootContainer {
case let .flat(container):
rootContainerNode = container
case let .split(container):
rootContainerNode = container
}
transition.updateSublayerTransformScaleAndOffset(node: rootContainerNode, scale: 1.0, offset: CGPoint())
}
}
case .regular:
if let rootModalFrame = self.rootModalFrame {
self.rootModalFrame = nil
rootModalFrame.updateDismissal(transition: .immediate, progress: 1.0, additionalProgress: 0.0, completion: { [weak rootModalFrame] in
rootModalFrame?.removeFromSupernode()
})
}
if let rootContainer = self.rootContainer {
var rootContainerNode: ASDisplayNode
switch rootContainer {
case let .flat(container):
rootContainerNode = container
case let .split(container):
rootContainerNode = container
}
ContainedViewLayoutTransition.immediate.updateSublayerTransformScaleAndOffset(node: rootContainerNode, scale: 1.0, offset: CGPoint())
}
}
if let minimizedContainer = self.minimizedContainer {
minimizedContainer.frame = CGRect(origin: .zero, size: layout.size)
minimizedContainer.updateLayout(layout, transition: transition)
}
if self.inCallStatusBar != nil {
statusBarStyle = .White
}
if let topVisibleOverlayContainerWithStatusBar = topVisibleOverlayContainerWithStatusBar {
statusBarStyle = topVisibleOverlayContainerWithStatusBar.controller.statusBar.statusBarStyle
}
if let topVisibleModalContainerWithStatusBar = topVisibleModalContainerWithStatusBar {
statusBarStyle = topVisibleModalContainerWithStatusBar.container.statusBarStyle
}
if self.currentStatusBarExternalHidden {
statusBarHidden = true
}
let resolvedStatusBarStyle: NavigationStatusBarStyle
switch statusBarStyle {
case .Ignore, .Hide:
if self.inCallStatusBar != nil {
resolvedStatusBarStyle = .white
} else {
resolvedStatusBarStyle = self.theme.statusBar
}
case .Black:
resolvedStatusBarStyle = .black
case .White:
resolvedStatusBarStyle = .white
}
if self.validStatusBarStyle != resolvedStatusBarStyle {
self.validStatusBarStyle = resolvedStatusBarStyle
let normalStatusBarStyle: UIStatusBarStyle
switch resolvedStatusBarStyle {
case .black:
if #available(iOS 13.0, *) {
normalStatusBarStyle = .darkContent
} else {
normalStatusBarStyle = .default
}
case .white:
normalStatusBarStyle = .lightContent
}
self.statusBarHost?.setStatusBarStyle(normalStatusBarStyle, animated: animateStatusBarStyleTransition)
}
if self.validStatusBarHidden != statusBarHidden {
self.validStatusBarHidden = statusBarHidden
self.statusBarHost?.setStatusBarHidden(statusBarHidden, animated: animateStatusBarStyleTransition)
}
var topHasOpaque = false
var foundControllerInFocus = false
for container in self.globalOverlayContainers.reversed() {
let controller = container.controller
if topHasOpaque {
controller.displayNode.accessibilityElementsHidden = true
} else {
if controller.isOpaqueWhenInOverlay || controller.blocksBackgroundWhenInOverlay {
topHasOpaque = true
}
controller.displayNode.accessibilityElementsHidden = false
}
}
for container in self.overlayContainers.reversed() {
if foundControllerInFocus {
container.controller.isInFocus = false
} else if container.controller.acceptsFocusWhenInOverlay {
foundControllerInFocus = true
container.controller.isInFocus = true
}
let controller = container.controller
if topHasOpaque {
controller.displayNode.accessibilityElementsHidden = true
} else {
if controller.isOpaqueWhenInOverlay || controller.blocksBackgroundWhenInOverlay {
topHasOpaque = true
}
controller.displayNode.accessibilityElementsHidden = false
}
}
for container in self.modalContainers.reversed() {
if foundControllerInFocus {
container.container.isInFocus = false
} else {
foundControllerInFocus = true
container.container.isInFocus = true
}
if let controller = container.container.controllers.last {
if topHasOpaque {
controller.displayNode.accessibilityElementsHidden = true
} else {
if controller.isOpaqueWhenInOverlay || controller.blocksBackgroundWhenInOverlay {
topHasOpaque = true
}
controller.displayNode.accessibilityElementsHidden = false
}
}
}
if let rootContainer = self.rootContainer {
switch rootContainer {
case let .flat(container):
if foundControllerInFocus {
container.isInFocus = false
} else {
foundControllerInFocus = true
container.isInFocus = true
}
if let controller = container.controllers.last {
if topHasOpaque {
controller.displayNode.accessibilityElementsHidden = true
} else {
if controller.isOpaqueWhenInOverlay || controller.blocksBackgroundWhenInOverlay {
topHasOpaque = true
}
controller.displayNode.accessibilityElementsHidden = false
}
}
case let .split(split):
if foundControllerInFocus {
split.isInFocus = false
} else {
foundControllerInFocus = true
split.isInFocus = true
}
var masterTopHasOpaque = topHasOpaque
var detailTopHasOpaque = topHasOpaque
if let controller = split.masterControllers.last {
if masterTopHasOpaque {
controller.displayNode.accessibilityElementsHidden = true
} else {
if controller.isOpaqueWhenInOverlay || controller.blocksBackgroundWhenInOverlay {
masterTopHasOpaque = true
}
controller.displayNode.accessibilityElementsHidden = false
}
}
if let controller = split.detailControllers.last {
if detailTopHasOpaque {
controller.displayNode.accessibilityElementsHidden = true
} else {
if controller.isOpaqueWhenInOverlay || controller.blocksBackgroundWhenInOverlay {
detailTopHasOpaque = true
}
controller.displayNode.accessibilityElementsHidden = false
}
}
}
}
self.isUpdatingContainers = false
if notifyGlobalOverlayControllersUpdated {
self.internalGlobalOverlayControllersUpdated()
}
self.updateSupportedOrientations?()
let updatedPrefersOnScreenNavigationHidden = self.collectPrefersOnScreenNavigationHidden()
if initialPrefersOnScreenNavigationHidden != updatedPrefersOnScreenNavigationHidden {
self.currentWindow?.invalidatePrefersOnScreenNavigationHidden()
}
if let externalCompletion {
if transition.isAnimated {
transition.attachAnimation(view: self.view, id: "updateContainers", completion: { _ in
externalCompletion()
})
} else {
externalCompletion()
}
}
}
private func controllerRemoved(_ controller: ViewController) {
self.filterController(controller, animated: false)
}
private func scrollToTop(_ subject: NavigationSplitContainerScrollToTop) {
if let _ = self.inCallStatusBar {
self.inCallNavigate?()
} else if let rootContainer = self.rootContainer {
if let modalContainer = self.modalContainers.last {
modalContainer.container.controllers.last?.scrollToTop?()
} else {
switch rootContainer {
case let .flat(container):
container.controllers.last?.scrollToTop?()
case let .split(container):
switch subject {
case .master:
container.masterControllers.last?.scrollToTop?()
case .detail:
container.detailControllers.last?.scrollToTop?()
}
}
}
}
}
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 = NavigationControllerNode(controller: self)
self.view = self.displayNode.view
self.view.clipsToBounds = true
self.view.autoresizingMask = []
self.displayNode.backgroundColor = self.theme.emptyAreaColor
if #available(iOSApplicationExtension 11.0, iOS 11.0, *) {
self.navigationBar.prefersLargeTitles = false
}
self.navigationBar.removeFromSuperview()
let globalScrollToTopNode = ScrollToTopNode(action: { [weak self] in
self?.scrollToTop(.master)
})
self.displayNode.addSubnode(globalScrollToTopNode)
self.globalScrollToTopNode = globalScrollToTopNode
let globalOverlayBelowKeyboardContainerParent = GlobalOverlayContainerParent()
self.displayNode.addSubnode(globalOverlayBelowKeyboardContainerParent)
self.globalOverlayBelowKeyboardContainerParent = globalOverlayBelowKeyboardContainerParent
let globalOverlayContainerParent = GlobalOverlayContainerParent()
self.displayNode.addSubnode(globalOverlayContainerParent)
self.globalOverlayContainerParent = globalOverlayContainerParent
if let inCallStatusBar = self.inCallStatusBar, inCallStatusBar.supernode == nil {
if let globalScrollToTopNode = self.globalScrollToTopNode {
self.displayNode.insertSubnode(inCallStatusBar, belowSubnode: globalScrollToTopNode)
} else {
self.displayNode.addSubnode(inCallStatusBar)
}
}
}
public func pushViewController(_ controller: ViewController) {
self.pushViewController(controller, completion: {})
}
public func pushViewController(_ controller: ViewController, animated: Bool = true, completion: @escaping () -> Void) {
var controllers = self.viewControllers
controllers.append(controller)
self.setViewControllers(controllers, animated: animated, completion: completion)
}
open override func pushViewController(_ viewController: UIViewController, animated: Bool) {
var controllers = self.viewControllers
controllers.append(viewController)
self.setViewControllers(controllers, animated: animated)
}
public func replaceTopController(_ controller: ViewController, animated: Bool, ready: Promise<Bool>? = nil) {
ready?.set(.single(true))
var controllers = self.viewControllers
controllers.removeLast()
controllers.append(controller)
self.setViewControllers(controllers, animated: animated)
}
public func filterController(_ controller: ViewController, animated: Bool) {
let controllers = self.viewControllers.filter({ $0 !== controller })
if controllers.count != self.viewControllers.count {
if controller.isViewLoaded && viewTreeContainsFirstResponder(view: controller.view) {
self.ignoreInputHeight = true
}
self.setViewControllers(controllers, animated: animated)
self.ignoreInputHeight = false
}
}
public func replaceController(_ controller: ViewController, with other: ViewController, animated: Bool) {
var controllers = self._viewControllers
for i in 0 ..< controllers.count {
if controllers[i] === controller {
controllers[i] = other
break
}
}
self.setViewControllers(controllers, animated: animated)
}
public func replaceControllersAndPush(controllers: [UIViewController], controller: ViewController, animated: Bool, options: NavigationAnimationOptions = [], ready: ValuePromise<Bool>? = nil, completion: @escaping () -> Void = {}) {
ready?.set(true)
let action = { [weak self] in
guard let self else {
return
}
var controllers = controllers
controllers.append(controller)
self.setViewControllers(controllers, animated: animated)
completion()
}
if let rootContainer = self.rootContainer, case let .split(container) = rootContainer, let topController = container.detailControllers.last {
if topController.attemptNavigation({
action()
}) {
action()
}
} else {
action()
}
}
public func replaceControllers(controllers: [UIViewController], animated: Bool, options: NavigationAnimationOptions = [], ready: ValuePromise<Bool>? = nil, completion: @escaping () -> Void = {}) {
ready?.set(true)
self.setViewControllers(controllers, animated: animated)
completion()
}
public func replaceAllButRootController(_ controller: ViewController, animated: Bool, animationOptions: NavigationAnimationOptions = [], ready: ValuePromise<Bool>? = nil, completion: @escaping () -> Void = {}) {
ready?.set(true)
var controllers = self.viewControllers
while controllers.count > 1 {
controllers.removeLast()
}
controllers.append(controller)
self.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) {
self.setViewControllers(viewControllers, animated: animated, completion: {})
}
public func setViewControllers(_ viewControllers: [UIViewController], animated: Bool, completion: @escaping () -> Void) {
for i in 0 ..< viewControllers.count {
guard let controller = viewControllers[i] as? ViewController else {
continue
}
if self.viewControllers.contains(where: { $0 === controller }) {
continue
}
if let customNavigationData = controller.customNavigationData {
var found = false
for previousIndex in (0 ..< self.viewControllers.count).reversed() {
let previousController = self.viewControllers[previousIndex]
if let previousController = previousController as? ViewController, let previousCustomNavigationDataSummary = previousController.customNavigationDataSummary {
controller.customNavigationDataSummary = customNavigationData.combine(summary: previousCustomNavigationDataSummary)
found = true
break
}
}
if !found {
controller.customNavigationDataSummary = customNavigationData.combine(summary: nil)
}
}
}
self._viewControllers = viewControllers.map { controller in
let controller = controller as! ViewController
controller.navigation_setNavigationController(self)
return controller
}
if let layout = self.validLayout {
self.updateContainers(layout: layout, transition: animated ? .animated(duration: 0.5, curve: .spring) : .immediate, completion: { [weak self] in
self?.notifyAccessibilityScreenChanged()
completion()
})
} else {
completion()
}
self._viewControllersPromise.set(self.viewControllers)
}
public func minimizeViewController(_ viewController: ViewController, damping: CGFloat?, velocity: CGFloat? = nil, setupContainer: (MinimizedContainer?) -> MinimizedContainer?, animated: Bool) {
let transition: ContainedViewLayoutTransition = animated ? .animated(duration: 0.4, curve: .customSpring(damping: damping ?? 124.0, initialVelocity: velocity ?? 0.0)) : .immediate
let minimizedContainer = setupContainer(self.minimizedContainer)
if self.minimizedContainer !== minimizedContainer {
minimizedContainer?.willMaximize = { [weak self] in
guard let self else {
return
}
self.isMaximizing = true
self.updateContainersNonReentrant(transition: .animated(duration: 0.4, curve: .spring))
}
self.minimizedContainer?.removeFromSupernode()
self.minimizedContainer = minimizedContainer
if let minimizedContainer {
if let modalContainer = self.modalContainers.first {
self.displayNode.insertSubnode(minimizedContainer, belowSubnode: modalContainer)
} else {
self.displayNode.addSubnode(minimizedContainer)
}
}
self.updateContainersNonReentrant(transition: transition)
}
viewController.isMinimized = true
self.filterController(viewController, animated: true)
minimizedContainer?.addController(viewController, transition: transition)
}
private var isMaximizing = false
public func maximizeViewController(_ viewController: ViewController, animated: Bool) {
guard let minimizedContainer = self.minimizedContainer else {
return
}
if animated {
self.isMaximizing = true
self.updateContainersNonReentrant(transition: .animated(duration: 0.4, curve: .spring))
}
minimizedContainer.maximizeController(viewController, animated: animated, completion: { [weak self] dismissed in
guard let self else {
return
}
var viewControllers = self.viewControllers
viewControllers.append(viewController)
self.setViewControllers(viewControllers, animated: false)
viewController.isMinimized = false
self.isMaximizing = false
if dismissed, let minimizedContainer = self.minimizedContainer {
self.minimizedContainer = nil
minimizedContainer.removeFromSupernode()
}
})
}
public func dismissMinimizedControllers(animated: Bool) {
guard let minimizedContainer = self.minimizedContainer else {
return
}
self.minimizedContainer = nil
minimizedContainer.dismissAll(completion: { [weak minimizedContainer] in
minimizedContainer?.removeFromSupernode()
})
self.updateContainersNonReentrant(transition: animated ? .animated(duration: 0.4, curve: .spring) : .immediate)
}
public var _keepModalDismissProgress = false
public func presentOverlay(controller: ViewController, inGlobal: Bool = false, blockInteraction: Bool = false) {
let container = NavigationOverlayContainer(controller: controller, blocksInteractionUntilReady: blockInteraction, controllerRemoved: { [weak self] controller in
guard let strongSelf = self else {
return
}
for i in 0 ..< strongSelf.globalOverlayContainers.count {
let overlayContainer = strongSelf.globalOverlayContainers[i]
if overlayContainer.controller === controller {
overlayContainer.isRemoved = true
overlayContainer.removeFromSupernode()
strongSelf.globalOverlayContainers.remove(at: i)
strongSelf.internalGlobalOverlayControllersUpdated()
break
}
}
for i in 0 ..< strongSelf.overlayContainers.count {
let overlayContainer = strongSelf.overlayContainers[i]
if overlayContainer.controller === controller {
overlayContainer.isRemoved = true
overlayContainer.removeFromSupernode()
strongSelf.overlayContainers.remove(at: i)
strongSelf._overlayControllersPromise.set(strongSelf.overlayContainers.map({ $0.controller }))
strongSelf.internalOverlayControllersUpdated()
break
}
}
strongSelf.updateContainersNonReentrant(transition: .immediate)
}, statusBarUpdated: { [weak self] transition in
guard let strongSelf = self else {
return
}
strongSelf.updateContainersNonReentrant(transition: transition)
}, modalStyleOverlayTransitionFactorUpdated: { [weak self] transition in
guard let strongSelf = self else {
return
}
strongSelf.updateContainersNonReentrant(transition: transition)
})
if inGlobal {
self.globalOverlayContainers.append(container)
} else {
self.overlayContainers.append(container)
self._overlayControllersPromise.set(self.overlayContainers.map({ $0.controller }))
}
container.isReadyUpdated = { [weak self, weak container] in
guard let strongSelf = self, let _ = container else {
return
}
strongSelf.updateContainersNonReentrant(transition: .immediate)
}
if let layout = self.validLayout {
self.updateContainers(layout: layout, transition: .immediate)
}
}
func updateExternalStatusBarHidden(_ value: Bool, transition: ContainedViewLayoutTransition) {
if self.currentStatusBarExternalHidden != value {
self.currentStatusBarExternalHidden = value
if let layout = self.validLayout {
self.updateContainers(layout: layout, transition: transition)
}
}
}
public func updatePossibleControllerDropContent(content: NavigationControllerDropContent?) {
if let rootContainer = self.rootContainer {
switch rootContainer {
case let .flat(container):
if let controller = container.controllers.last {
controller.updatePossibleControllerDropContent(content: content)
}
case .split:
break
}
}
}
public func acceptPossibleControllerDropContent(content: NavigationControllerDropContent) -> Bool {
if let rootContainer = self.rootContainer {
switch rootContainer {
case let .flat(container):
if let controller = container.controllers.last {
if controller.acceptPossibleControllerDropContent(content: content) {
return true
}
}
case .split:
break
}
}
return false
}
override open func present(_ viewControllerToPresent: UIViewController, animated flag: Bool, completion: (() -> Void)? = nil) {
preconditionFailure()
}
override open func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) {
if let presentingViewController = self.presentingViewController {
presentingViewController.dismiss(animated: false, completion: nil)
}
if let controller = self.presentedViewController {
if flag {
UIView.animate(withDuration: 0.3, delay: 0.0, options: UIView.AnimationOptions(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 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()
}
public func requestLayout(transition: ContainedViewLayoutTransition) {
if self.isViewLoaded, let validLayout = self.validLayout {
self.containerLayoutUpdated(validLayout, transition: transition)
}
}
public func setForceInCallStatusBar(_ forceInCallStatusBar: CallStatusBarNode?, transition: ContainedViewLayoutTransition = .animated(duration: 0.3, curve: .easeInOut)) {
if let forceInCallStatusBar = forceInCallStatusBar {
let inCallStatusBar: StatusBar
if let current = self.inCallStatusBar {
inCallStatusBar = current
} else {
inCallStatusBar = StatusBar()
inCallStatusBar.clipsToBounds = false
inCallStatusBar.inCallNavigate = { [weak self] in
self?.scrollToTop(.master)
}
self.inCallStatusBar = inCallStatusBar
var bottomOverlayContainer: NavigationOverlayContainer?
for overlayContainer in self.overlayContainers {
if overlayContainer.supernode != nil {
bottomOverlayContainer = overlayContainer
break
}
}
if self._displayNode != nil {
if let bottomOverlayContainer = bottomOverlayContainer {
self.displayNode.insertSubnode(inCallStatusBar, belowSubnode: bottomOverlayContainer)
} else if let globalScrollToTopNode = self.globalScrollToTopNode {
self.displayNode.insertSubnode(inCallStatusBar, belowSubnode: globalScrollToTopNode)
} else {
self.displayNode.addSubnode(inCallStatusBar)
}
}
if case let .animated(duration, _) = transition {
inCallStatusBar.layer.animatePosition(from: CGPoint(x: 0.0, y: -64.0), to: CGPoint(), duration: duration, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, additive: true)
}
}
if let layout = self.validLayout {
inCallStatusBar.updateState(statusBar: nil, withSafeInsets: !layout.safeInsets.top.isZero, inCallNode: forceInCallStatusBar, animated: false)
self.containerLayoutUpdated(layout, transition: transition)
} else {
self.updateInCallStatusBarState = forceInCallStatusBar
}
} else if let inCallStatusBar = self.inCallStatusBar {
self.inCallStatusBar = nil
transition.updatePosition(node: inCallStatusBar, position: CGPoint(x: inCallStatusBar.position.x, y: -64.0), completion: { [weak inCallStatusBar] _ in
inCallStatusBar?.removeFromSupernode()
})
if let layout = self.validLayout {
self.containerLayoutUpdated(layout, transition: transition)
}
}
}
public var overlayControllers: [ViewController] {
return self.overlayContainers.compactMap { container in
if container.isReady {
return container.controller
} else {
return nil
}
}
}
public var globalOverlayControllers: [ViewController] {
return self.globalOverlayContainers.compactMap { container in
if container.isReady {
return container.controller
} else {
return nil
}
}
}
private func internalGlobalOverlayControllersUpdated() {
self.globalOverlayControllersUpdated?()
self.currentWindow?.invalidatePrefersOnScreenNavigationHidden()
}
private func internalOverlayControllersUpdated() {
self.currentWindow?.invalidatePrefersOnScreenNavigationHidden()
}
private func collectPrefersOnScreenNavigationHidden() -> Bool {
var hidden = false
if let overlayController = self.topOverlayController {
hidden = hidden || overlayController.prefersOnScreenNavigationHidden
}
return hidden
}
private func notifyAccessibilityScreenChanged() {
UIAccessibility.post(notification: UIAccessibility.Notification.screenChanged, argument: nil)
}
public func updateRootContainerTransitionOffset(_ offset: CGFloat, transition: ContainedViewLayoutTransition) {
guard let rootContainer = self.rootContainer, case let .flat(container) = rootContainer else {
return
}
transition.updateTransform(node: container, transform: CGAffineTransformMakeTranslation(offset, 0.0))
if let minimizedContainer = self.minimizedContainer {
transition.updateTransform(node: minimizedContainer, transform: CGAffineTransformMakeTranslation(offset, 0.0))
}
}
}