Merge commit '7621e2f8dec938cf48181c8b10afc9b01f444e68' into beta

This commit is contained in:
Ilya Laktyushin
2025-12-06 02:17:48 +04:00
commit 8344b97e03
28070 changed files with 7995182 additions and 0 deletions
@@ -0,0 +1,93 @@
import Foundation
import UIKit
import AsyncDisplayKit
public protocol MinimizedContainer: ASDisplayNode {
var navigationController: NavigationController? { get set }
var controllers: [MinimizableController] { get }
var isExpanded: Bool { get }
var willMaximize: ((MinimizedContainer) -> Void)? { get set }
var willDismiss: ((MinimizedContainer) -> Void)? { get set }
var didDismiss: ((MinimizedContainer) -> Void)? { get set }
var statusBarStyle: StatusBarStyle { get }
var statusBarStyleUpdated: (() -> Void)? { get set }
func addController(_ viewController: MinimizableController, topEdgeOffset: CGFloat?, beforeMaximize: @escaping (NavigationController, @escaping () -> Void) -> Void, transition: ContainedViewLayoutTransition)
func removeController(_ viewController: MinimizableController)
func maximizeController(_ viewController: MinimizableController, animated: Bool, completion: @escaping (Bool) -> Void)
func collapse()
func dismissAll(completion: @escaping () -> Void)
func updateLayout(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition)
func collapsedHeight(layout: ContainerViewLayout) -> CGFloat
}
public protocol MinimizableController: ViewController {
var minimizedTopEdgeOffset: CGFloat? { get }
var minimizedBounds: CGRect? { get }
var isMinimized: Bool { get set }
var isMinimizable: Bool { get }
var minimizedIcon: UIImage? { get }
var minimizedProgress: Float? { get }
var isFullscreen: Bool { get }
func requestMinimize(topEdgeOffset: CGFloat?, initialVelocity: CGFloat?)
func makeContentSnapshotView() -> UIView?
func prepareContentSnapshotView()
func resetContentSnapshotView()
func shouldDismissImmediately() -> Bool
}
public extension MinimizableController {
var isFullscreen: Bool {
return false
}
var minimizedTopEdgeOffset: CGFloat? {
return nil
}
var minimizedBounds: CGRect? {
return nil
}
var isMinimized: Bool {
return false
}
var isMinimizable: Bool {
return false
}
var minimizedIcon: UIImage? {
return nil
}
var minimizedProgress: Float? {
return nil
}
func requestMinimize(topEdgeOffset: CGFloat?, initialVelocity: CGFloat?) {
}
func makeContentSnapshotView() -> UIView? {
return self.displayNode.view.snapshotView(afterScreenUpdates: false)
}
func prepareContentSnapshotView() {
}
func resetContentSnapshotView() {
}
func shouldDismissImmediately() -> Bool {
return true
}
}
@@ -0,0 +1,689 @@
import Foundation
import UIKit
import AsyncDisplayKit
import SwiftSignalKit
public final class NavigationContainer: ASDisplayNode, ASGestureRecognizerDelegate {
private final class Child {
let value: ViewController
var layout: ContainerViewLayout
init(value: ViewController, layout: ContainerViewLayout) {
self.value = value
self.layout = layout
}
}
private final class PendingChild {
enum TransitionType {
case push
case pop
}
let value: Child
let transitionType: TransitionType
let transition: ContainedViewLayoutTransition
let disposable: MetaDisposable = MetaDisposable()
var isReady: Bool = false
init(value: Child, transitionType: TransitionType, transition: ContainedViewLayoutTransition, update: @escaping (PendingChild) -> Void) {
self.value = value
self.transitionType = transitionType
self.transition = transition
var localIsReady: Bool?
self.disposable.set((value.value.ready.get()
|> filter { $0 }
|> take(1)
|> deliverOnMainQueue).start(next: { [weak self] _ in
if localIsReady == nil {
localIsReady = true
} else if let strongSelf = self {
update(strongSelf)
}
}))
if let _ = localIsReady {
self.isReady = true
} else {
localIsReady = false
}
}
deinit {
self.disposable.dispose()
}
}
private final class TopTransition {
let type: PendingChild.TransitionType
let previous: Child
let coordinator: NavigationTransitionCoordinator
init(type: PendingChild.TransitionType, previous: Child, coordinator: NavigationTransitionCoordinator) {
self.type = type
self.previous = previous
self.coordinator = coordinator
}
}
private struct State {
var layout: ContainerViewLayout?
var canBeClosed: Bool?
var top: Child?
var transition: TopTransition?
var pending: PendingChild?
}
private let isFlat: Bool
public private(set) var controllers: [ViewController] = []
private var state: State = State(layout: nil, canBeClosed: nil, top: nil, transition: nil, pending: nil)
weak var minimizedContainer: MinimizedContainer?
private var ignoreInputHeight: Bool = false
public private(set) var isReady: Bool = false
public var isReadyUpdated: (() -> Void)?
public var controllerRemoved: (ViewController) -> Void
public var requestFilterController: (ViewController) -> Void = { _ in }
public var keyboardViewManager: KeyboardViewManager? {
didSet {
}
}
public var canHaveKeyboardFocus: Bool = false {
didSet {
if self.canHaveKeyboardFocus != oldValue {
if !self.canHaveKeyboardFocus {
self.view.endEditing(true)
self.performUpdate(transition: .animated(duration: 0.5, curve: .spring))
}
}
}
}
public var isInFocus: Bool = false {
didSet {
if self.isInFocus != oldValue {
self.inFocusUpdated(isInFocus: self.isInFocus)
}
}
}
public func inFocusUpdated(isInFocus: Bool) {
self.state.top?.value.isInFocus = isInFocus
}
public var overflowInset: CGFloat = 0.0
private var currentKeyboardLeftEdge: CGFloat = 0.0
private var additionalKeyboardLeftEdgeOffset: CGFloat = 0.0
var statusBarStyle: StatusBarStyle = .Ignore {
didSet {
}
}
var statusBarStyleUpdated: ((ContainedViewLayoutTransition) -> Void)?
private var panRecognizer: InteractiveTransitionGestureRecognizer?
public init(isFlat: Bool, controllerRemoved: @escaping (ViewController) -> Void) {
self.isFlat = isFlat
self.controllerRemoved = controllerRemoved
super.init()
}
public override func didLoad() {
super.didLoad()
let panRecognizer = InteractiveTransitionGestureRecognizer(target: self, action: #selector(self.panGesture(_:)), allowedDirections: { [weak self] _ in
guard let strongSelf = self, strongSelf.controllers.count > 1 else {
return []
}
return .right
})
/*panRecognizer.dynamicEdgeWidth = { [weak self] _ in
guard let self, let controller = self.controllers.last, let value = controller.interactiveNavivationGestureEdgeWidth else {
return .constant(16.0)
}
return value
}*/
if #available(iOS 13.4, *) {
panRecognizer.allowedScrollTypesMask = .continuous
}
panRecognizer.delegate = self.wrappedGestureRecognizerDelegate
panRecognizer.delaysTouchesBegan = false
panRecognizer.cancelsTouchesInView = true
self.panRecognizer = panRecognizer
self.view.addGestureRecognizer(panRecognizer)
/*self.view.disablesInteractiveTransitionGestureRecognizerNow = { [weak self] in
guard let strongSelf = self else {
return false
}
if strongSelf.state.transition != nil {
return true
}
return false
}*/
}
func hasNonReadyControllers() -> Bool {
if let pending = self.state.pending, !pending.isReady {
return true
}
return false
}
public override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
if gestureRecognizer == self.panRecognizer, let gestureRecognizer = self.panRecognizer, gestureRecognizer.numberOfTouches == 0 {
let translation = gestureRecognizer.velocity(in: gestureRecognizer.view)
if abs(translation.y) > 4.0 && abs(translation.y) > abs(translation.x) * 2.5 {
return false
}
if translation.x < 4.0 {
return false
}
if self.controllers.count == 1 {
return false
}
return true
} else {
return true
}
}
public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return false
}
public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldBeRequiredToFailBy otherGestureRecognizer: UIGestureRecognizer) -> Bool {
if let _ = otherGestureRecognizer as? InteractiveTransitionGestureRecognizer {
return false
}
if let _ = otherGestureRecognizer as? UIPanGestureRecognizer {
return true
}
return false
}
@objc private func panGesture(_ recognizer: UIPanGestureRecognizer) {
switch recognizer.state {
case .began:
guard let layout = self.state.layout else {
return
}
guard self.state.transition == nil else {
return
}
let beginGesture = self.controllers.count > 1
if beginGesture {
let topController = self.controllers[self.controllers.count - 1]
let bottomController = self.controllers[self.controllers.count - 2]
if !topController.attemptNavigation({ [weak self, weak topController] in
if let self, let topController {
self.requestFilterController(topController)
}
}) {
return
}
topController.viewWillDisappear(true)
let topNode = topController.displayNode
var bottomControllerLayout = layout
if bottomController.view.disableAutomaticKeyboardHandling.isEmpty {
if let minimizedContainer = self.minimizedContainer, (bottomControllerLayout.inputHeight ?? 0.0) > 0.0 {
var updatedSize = bottomControllerLayout.size
var updatedIntrinsicInsets = bottomControllerLayout.intrinsicInsets
updatedSize.height -= minimizedContainer.collapsedHeight(layout: layout)
updatedIntrinsicInsets.bottom = 0.0
bottomControllerLayout = bottomControllerLayout.withUpdatedSize(updatedSize).withUpdatedIntrinsicInsets(updatedIntrinsicInsets)
}
bottomControllerLayout = bottomControllerLayout.withUpdatedInputHeight(nil)
}
bottomController.containerLayoutUpdated(bottomControllerLayout, transition: .immediate)
bottomController.viewWillAppear(true)
let bottomNode = bottomController.displayNode
let screenCornerRadius = self.minimizedContainer == nil && self.state.canBeClosed != true ? layout.deviceMetrics.screenCornerRadius : 0.0
let navigationTransitionCoordinator = NavigationTransitionCoordinator(transition: .Pop, isInteractive: true, isFlat: self.isFlat, container: self, topNode: topNode, topNavigationBar: topController.transitionNavigationBar, bottomNode: bottomNode, bottomNavigationBar: bottomController.transitionNavigationBar, screenCornerRadius: screenCornerRadius, didUpdateProgress: { [weak self, weak bottomController] progress, transition, topFrame, bottomFrame in
if let strongSelf = self {
if let top = strongSelf.state.top {
strongSelf.syncKeyboard(leftEdge: top.value.displayNode.frame.minX, transition: transition)
var updatedStatusBarStyle = strongSelf.statusBarStyle
if let bottomController = bottomController, progress >= 0.3 {
updatedStatusBarStyle = bottomController.statusBar.statusBarStyle
} else {
updatedStatusBarStyle = top.value.statusBar.statusBarStyle
}
if strongSelf.statusBarStyle != updatedStatusBarStyle {
strongSelf.statusBarStyle = updatedStatusBarStyle
strongSelf.statusBarStyleUpdated?(.animated(duration: 0.3, curve: .easeInOut))
}
}
}
})
bottomController.displayNode.recursivelyEnsureDisplaySynchronously(true)
self.state.transition = TopTransition(type: .pop, previous: Child(value: bottomController, layout: layout), coordinator: navigationTransitionCoordinator)
}
case .changed:
if let navigationTransitionCoordinator = self.state.transition?.coordinator, !navigationTransitionCoordinator.animatingCompletion {
let translation = recognizer.translation(in: self.view).x
let progress = max(0.0, min(1.0, translation / self.view.frame.width))
navigationTransitionCoordinator.updateProgress(progress, transition: .immediate, completion: {})
}
case .ended, .cancelled:
if let navigationTransitionCoordinator = self.state.transition?.coordinator, !navigationTransitionCoordinator.animatingCompletion {
let velocity = recognizer.velocity(in: self.view).x
if velocity > 1000 || navigationTransitionCoordinator.progress > 0.2 {
self.state.top?.value.viewWillLeaveNavigation()
navigationTransitionCoordinator.animateCompletion(velocity, completion: { [weak self] in
guard let strongSelf = self, let _ = strongSelf.state.layout, let _ = strongSelf.state.transition, let top = strongSelf.state.top else {
return
}
let topController = top.value
if viewTreeContainsFirstResponder(view: top.value.view) {
strongSelf.ignoreInputHeight = true
}
strongSelf.keyboardViewManager?.dismissEditingWithoutAnimation(view: topController.view)
strongSelf.state.transition = nil
strongSelf.controllerRemoved(top.value)
strongSelf.ignoreInputHeight = false
})
} else {
navigationTransitionCoordinator.animateCancel({ [weak self] in
guard let strongSelf = self, let top = strongSelf.state.top, let transition = strongSelf.state.transition else {
return
}
strongSelf.state.transition = nil
top.value.viewDidAppear(true)
transition.previous.value.viewDidDisappear(true)
})
}
}
default:
break
}
}
public func update(layout: ContainerViewLayout, canBeClosed: Bool, controllers: [ViewController], transition: ContainedViewLayoutTransition) {
self.state.layout = layout
self.state.canBeClosed = canBeClosed
var controllersUpdated = false
if self.controllers.count != controllers.count {
controllersUpdated = true
} else {
for i in 0 ..< controllers.count {
if self.controllers[i] !== controllers[i] {
controllersUpdated = true
break
}
}
}
if controllersUpdated {
let previousControllers = self.controllers
self.controllers = controllers
for i in 0 ..< controllers.count {
if i == 0 {
if canBeClosed {
controllers[i].transitionNavigationBar?.previousItem = .close
controllers[i].previousItem = .close
} else {
controllers[i].transitionNavigationBar?.previousItem = nil
controllers[i].previousItem = nil
}
} else {
controllers[i].transitionNavigationBar?.previousItem = .item(controllers[i - 1].navigationItem)
controllers[i].previousItem = .item(controllers[i - 1].navigationItem)
}
}
if controllers.last !== self.state.top?.value {
self.state.top?.value.statusBar.alphaUpdated = nil
if let controller = controllers.last {
controller.statusBar.alphaUpdated = { [weak self, weak controller] transition in
guard let strongSelf = self, let controller = controller else {
return
}
if strongSelf.state.top?.value === controller && strongSelf.state.transition == nil {
strongSelf.statusBarStyleUpdated?(transition)
}
}
}
if controllers.last !== self.state.pending?.value.value {
self.state.pending = nil
if let last = controllers.last {
let transitionType: PendingChild.TransitionType
if !previousControllers.contains(where: { $0 === last }) {
transitionType = .push
} else {
transitionType = .pop
}
var updatedLayout = layout
if last.view.disableAutomaticKeyboardHandling.isEmpty {
updatedLayout = updatedLayout.withUpdatedInputHeight(nil)
}
self.state.pending = PendingChild(value: self.makeChild(layout: updatedLayout, value: last), transitionType: transitionType, transition: transition, update: { [weak self] pendingChild in
self?.pendingChildIsReady(pendingChild)
})
}
}
}
}
var statusBarTransition = transition
var ignoreInputHeight = false
if let pending = self.state.pending {
if pending.isReady {
self.state.pending = nil
let previous = self.state.top
previous?.value.isInFocus = false
self.state.top = pending.value
var updatedLayout = layout
if pending.value.value.view.disableAutomaticKeyboardHandling.isEmpty {
updatedLayout = updatedLayout.withUpdatedInputHeight(nil)
}
if case .regular = layout.metrics.widthClass, pending.value.layout.inputHeight == nil {
ignoreInputHeight = true
}
self.topTransition(from: previous, to: pending.value, transitionType: pending.transitionType, layout: updatedLayout, transition: pending.transition)
self.state.top?.value.isInFocus = self.isInFocus
statusBarTransition = pending.transition
if !self.isReady {
self.isReady = true
self.isReadyUpdated?()
}
}
}
if controllers.isEmpty && self.state.top != nil {
let previous = self.state.top
previous?.value.isInFocus = false
self.state.top = nil
self.topTransition(from: previous, to: nil, transitionType: .pop, layout: layout, transition: .immediate)
}
var updatedStatusBarStyle = self.statusBarStyle
if let top = self.state.top {
var updatedLayout = layout
if let _ = self.state.transition, top.value.view.disableAutomaticKeyboardHandling.isEmpty {
if !viewTreeContainsFirstResponder(view: top.value.view) {
updatedLayout = updatedLayout.withUpdatedInputHeight(nil)
}
}
if ignoreInputHeight {
updatedLayout = updatedLayout.withUpdatedInputHeight(nil)
}
self.applyLayout(layout: updatedLayout, to: top, isMaster: true, transition: transition)
if let childTransition = self.state.transition, childTransition.coordinator.isInteractive {
switch childTransition.type {
case .push:
if childTransition.coordinator.progress >= 0.3 {
updatedStatusBarStyle = top.value.statusBar.statusBarStyle
} else {
updatedStatusBarStyle = childTransition.previous.value.statusBar.statusBarStyle
}
case .pop:
if childTransition.coordinator.progress >= 0.3 {
updatedStatusBarStyle = childTransition.previous.value.statusBar.statusBarStyle
} else {
updatedStatusBarStyle = top.value.statusBar.statusBarStyle
}
}
} else {
updatedStatusBarStyle = top.value.statusBar.statusBarStyle
}
} else {
updatedStatusBarStyle = .Ignore
}
if self.statusBarStyle != updatedStatusBarStyle {
self.statusBarStyle = updatedStatusBarStyle
self.statusBarStyleUpdated?(statusBarTransition)
}
}
public var shouldAnimateDisappearance: Bool = false
private func topTransition(from fromValue: Child?, to toValue: Child?, transitionType: PendingChild.TransitionType, layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
if case .animated = transition, let fromValue = fromValue, let toValue = toValue {
if let currentTransition = self.state.transition {
currentTransition.coordinator.performCompletion(completion: {
})
}
fromValue.value.viewWillLeaveNavigation()
fromValue.value.viewWillDisappear(true)
toValue.value.viewWillAppear(true)
toValue.value.setIgnoreAppearanceMethodInvocations(true)
if let layout = self.state.layout {
toValue.value.displayNode.frame = CGRect(origin: CGPoint(), size: layout.size)
}
let mappedTransitionType: NavigationTransition
let topController: ViewController
let bottomController: ViewController
switch transitionType {
case .push:
mappedTransitionType = .Push
self.addSubnode(toValue.value.displayNode)
topController = toValue.value
bottomController = fromValue.value
case .pop:
mappedTransitionType = .Pop
self.insertSubnode(toValue.value.displayNode, belowSubnode: fromValue.value.displayNode)
topController = fromValue.value
bottomController = toValue.value
}
toValue.value.setIgnoreAppearanceMethodInvocations(false)
let screenCornerRadius = self.minimizedContainer == nil && self.state.canBeClosed != true ? layout.deviceMetrics.screenCornerRadius : 0.0
let topTransition = TopTransition(type: transitionType, previous: fromValue, coordinator: NavigationTransitionCoordinator(transition: mappedTransitionType, isInteractive: false, isFlat: self.isFlat, container: self, topNode: topController.displayNode, topNavigationBar: topController.transitionNavigationBar, bottomNode: bottomController.displayNode, bottomNavigationBar: bottomController.transitionNavigationBar, screenCornerRadius: screenCornerRadius, didUpdateProgress: { [weak self] _, transition, topFrame, bottomFrame in
guard let strongSelf = self else {
return
}
switch transitionType {
case .push:
if let _ = strongSelf.state.transition, let top = strongSelf.state.top, viewTreeContainsFirstResponder(view: top.value.view) {
strongSelf.syncKeyboard(leftEdge: topFrame.minX, transition: transition)
} else {
if let hasActiveInput = strongSelf.state.top?.value.hasActiveInput, hasActiveInput {
} else {
strongSelf.syncKeyboard(leftEdge: topFrame.minX - bottomFrame.width, transition: transition)
}
}
case .pop:
strongSelf.syncKeyboard(leftEdge: topFrame.minX, transition: transition)
}
}))
self.state.transition = topTransition
topTransition.coordinator.animateCompletion(0.0, completion: { [weak self, weak topTransition] in
guard let strongSelf = self, let topTransition = topTransition, strongSelf.state.transition === topTransition else {
return
}
if viewTreeContainsFirstResponder(view: topTransition.previous.value.view) {
strongSelf.ignoreInputHeight = true
}
strongSelf.keyboardViewManager?.dismissEditingWithoutAnimation(view: topTransition.previous.value.view)
strongSelf.state.transition = nil
if strongSelf.shouldAnimateDisappearance {
let displayNode = topTransition.previous.value.displayNode
displayNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak displayNode] _ in
displayNode?.removeFromSupernode()
displayNode?.layer.removeAllAnimations()
})
} else {
topTransition.previous.value.setIgnoreAppearanceMethodInvocations(true)
topTransition.previous.value.displayNode.removeFromSupernode()
topTransition.previous.value.setIgnoreAppearanceMethodInvocations(false)
}
topTransition.previous.value.viewDidDisappear(true)
if let toValue = strongSelf.state.top, let layout = strongSelf.state.layout {
toValue.value.displayNode.frame = CGRect(origin: CGPoint(), size: layout.size)
strongSelf.applyLayout(layout: layout, to: toValue, isMaster: true, transition: .immediate)
toValue.value.viewDidAppear(true)
}
strongSelf.ignoreInputHeight = false
})
} else {
if let fromValue = fromValue {
if viewTreeContainsFirstResponder(view: fromValue.value.view) {
self.ignoreInputHeight = true
}
fromValue.value.viewWillLeaveNavigation()
fromValue.value.viewWillDisappear(false)
self.keyboardViewManager?.dismissEditingWithoutAnimation(view: fromValue.value.view)
fromValue.value.setIgnoreAppearanceMethodInvocations(true)
fromValue.value.displayNode.removeFromSupernode()
fromValue.value.setIgnoreAppearanceMethodInvocations(false)
fromValue.value.viewDidDisappear(false)
}
if let toValue = toValue {
self.applyLayout(layout: layout, to: toValue, isMaster: true, transition: .immediate)
toValue.value.displayNode.frame = CGRect(origin: CGPoint(), size: layout.size)
toValue.value.viewWillAppear(false)
toValue.value.setIgnoreAppearanceMethodInvocations(true)
self.addSubnode(toValue.value.displayNode)
toValue.value.setIgnoreAppearanceMethodInvocations(false)
toValue.value.displayNode.recursivelyEnsureDisplaySynchronously(true)
toValue.value.viewDidAppear(false)
}
self.ignoreInputHeight = false
}
}
private func makeChild(layout: ContainerViewLayout, value: ViewController) -> Child {
var updatedLayout = layout
if value.view.disableAutomaticKeyboardHandling.isEmpty {
updatedLayout = updatedLayout.withUpdatedInputHeight(nil)
}
value.containerLayoutUpdated(updatedLayout, transition: .immediate)
return Child(value: value, layout: updatedLayout)
}
private func applyLayout(layout: ContainerViewLayout, to child: Child, isMaster: Bool, transition: ContainedViewLayoutTransition) {
var childFrame = CGRect(origin: CGPoint(), size: layout.size)
var updatedLayout = layout
var shouldSyncKeyboard = false
if let transition = self.state.transition {
childFrame.origin.x = child.value.displayNode.frame.origin.x
switch transition.type {
case .pop:
if transition.previous.value === child.value {
shouldSyncKeyboard = true
}
case .push:
break
}
if updatedLayout.inputHeight != nil {
if !self.canHaveKeyboardFocus && child.value.view.disableAutomaticKeyboardHandling.isEmpty {
updatedLayout = updatedLayout.withUpdatedInputHeight(nil)
}
}
} else {
if isMaster {
shouldSyncKeyboard = true
}
if updatedLayout.inputHeight != nil && child.value.view.disableAutomaticKeyboardHandling.isEmpty {
if !self.canHaveKeyboardFocus || self.ignoreInputHeight {
updatedLayout = updatedLayout.withUpdatedInputHeight(nil)
}
}
}
if child.value.displayNode.frame != childFrame {
transition.updateFrame(node: child.value.displayNode, frame: childFrame)
}
if shouldSyncKeyboard && isMaster {
self.syncKeyboard(leftEdge: childFrame.minX, transition: transition)
}
if child.layout != updatedLayout {
child.layout = updatedLayout
child.value.containerLayoutUpdated(updatedLayout, transition: transition)
}
}
public func updateAdditionalKeyboardLeftEdgeOffset(_ offset: CGFloat, transition: ContainedViewLayoutTransition) {
self.additionalKeyboardLeftEdgeOffset = offset
self.syncKeyboard(leftEdge: self.currentKeyboardLeftEdge, transition: transition)
}
private func syncKeyboard(leftEdge: CGFloat, transition: ContainedViewLayoutTransition) {
self.currentKeyboardLeftEdge = leftEdge
self.keyboardViewManager?.update(leftEdge: self.additionalKeyboardLeftEdgeOffset + leftEdge, transition: transition)
}
private func pendingChildIsReady(_ child: PendingChild) {
if let pending = self.state.pending, pending === child {
pending.isReady = true
self.performUpdate(transition: .immediate)
}
}
private func performUpdate(transition: ContainedViewLayoutTransition) {
if let layout = self.state.layout, let canBeClosed = self.state.canBeClosed {
self.update(layout: layout, canBeClosed: canBeClosed, controllers: self.controllers, transition: transition)
}
}
public override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if !self.bounds.contains(point) {
return nil
}
if self.state.transition != nil {
return self.view
}
return super.hitTest(point, with: event)
}
func combinedSupportedOrientations(currentOrientationToLock: UIInterfaceOrientationMask) -> ViewControllerSupportedOrientations {
var supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .allButUpsideDown)
if let controller = self.controllers.last {
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)
}
}
if let transition = self.state.transition {
let controller = transition.previous.value
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
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,121 @@
import Foundation
import UIKit
import AsyncDisplayKit
import SwiftSignalKit
enum RootNavigationLayout {
case split([ViewController], [ViewController])
case flat([ViewController])
}
struct ModalContainerLayout {
var controllers: [ViewController]
var isFlat: Bool
var flatReceivesModalTransition: Bool
var isStandalone: Bool
}
struct NavigationLayout {
var root: RootNavigationLayout
var modal: [ModalContainerLayout]
}
func makeNavigationLayout(mode: NavigationControllerMode, layout: ContainerViewLayout, controllers: [ViewController]) -> NavigationLayout {
var rootControllers: [ViewController] = []
var modalStack: [ModalContainerLayout] = []
for controller in controllers {
let requiresModal: Bool
var beginsModal: Bool = false
var isFlat: Bool = false
var flatReceivesModalTransition: Bool = false
var isStandalone: Bool = false
switch controller.navigationPresentation {
case .default:
requiresModal = false
case .master:
requiresModal = false
case .modal:
requiresModal = true
beginsModal = true
case .flatModal:
requiresModal = true
beginsModal = true
isFlat = true
flatReceivesModalTransition = controller.flatReceivesModalTransition
case .standaloneModal:
requiresModal = true
beginsModal = true
isStandalone = true
case .standaloneFlatModal:
requiresModal = true
beginsModal = true
isStandalone = true
isFlat = true
case .modalInLargeLayout:
switch layout.metrics.widthClass {
case .compact:
requiresModal = false
case .regular:
requiresModal = true
}
case .modalInCompactLayout:
switch layout.metrics.widthClass {
case .compact:
requiresModal = true
case .regular:
requiresModal = true
beginsModal = true
isFlat = true
}
}
if requiresModal {
controller._presentedInModal = true
if beginsModal || modalStack.isEmpty || modalStack[modalStack.count - 1].isStandalone {
modalStack.append(ModalContainerLayout(controllers: [controller], isFlat: isFlat, flatReceivesModalTransition: flatReceivesModalTransition, isStandalone: isStandalone))
} else {
modalStack[modalStack.count - 1].controllers.append(controller)
}
} else if !modalStack.isEmpty {
if modalStack[modalStack.count - 1].isFlat {
} else {
controller._presentedInModal = true
}
if modalStack[modalStack.count - 1].isStandalone {
modalStack.append(ModalContainerLayout(controllers: [controller], isFlat: isFlat, flatReceivesModalTransition: flatReceivesModalTransition, isStandalone: isStandalone))
} else {
modalStack[modalStack.count - 1].controllers.append(controller)
}
} else {
controller._presentedInModal = false
rootControllers.append(controller)
}
}
let rootLayout: RootNavigationLayout
switch mode {
case .single:
rootLayout = .flat(rootControllers)
case .automaticMasterDetail:
switch layout.metrics.widthClass {
case .compact:
rootLayout = .flat(rootControllers)
case .regular:
let masterControllers = rootControllers.filter {
if case .master = $0.navigationPresentation {
return true
} else {
return false
}
}
let detailControllers = rootControllers.filter {
if case .master = $0.navigationPresentation {
return false
} else {
return true
}
}
rootLayout = .split(masterControllers, detailControllers)
}
}
return NavigationLayout(root: rootLayout, modal: modalStack)
}
@@ -0,0 +1,610 @@
import Foundation
import UIKit
import AsyncDisplayKit
import SwiftSignalKit
import UIKitRuntimeUtils
final class NavigationModalContainer: ASDisplayNode, ASScrollViewDelegate, ASGestureRecognizerDelegate {
private var theme: NavigationControllerTheme
let isFlat: Bool
private let dim: ASDisplayNode
private let scrollNode: ASScrollNode
let container: NavigationContainer
private var panRecognizer: InteractiveTransitionGestureRecognizer?
private(set) var isReady: Bool = false
private(set) var dismissProgress: CGFloat = 0.0
var isReadyUpdated: (() -> Void)?
var updateDismissProgress: ((CGFloat, ContainedViewLayoutTransition) -> Void)?
var interactivelyDismissed: ((Bool) -> Void)?
private var isUpdatingState = false
private var ignoreScrolling = false
private var isDismissed = false
private var isInteractiveDimissEnabled = true
private var validLayout: ContainerViewLayout?
private var horizontalDismissOffset: CGFloat?
var keyboardViewManager: KeyboardViewManager? {
didSet {
if self.keyboardViewManager !== oldValue {
self.container.keyboardViewManager = self.keyboardViewManager
}
}
}
var canHaveKeyboardFocus: Bool = false {
didSet {
self.container.canHaveKeyboardFocus = self.canHaveKeyboardFocus
}
}
init(theme: NavigationControllerTheme, isFlat: Bool, controllerRemoved: @escaping (ViewController) -> Void) {
self.theme = theme
self.isFlat = isFlat
self.dim = ASDisplayNode()
self.dim.alpha = 0.0
self.scrollNode = ASScrollNode()
self.container = NavigationContainer(isFlat: false, controllerRemoved: controllerRemoved)
self.container.clipsToBounds = true
super.init()
self.addSubnode(self.dim)
self.addSubnode(self.scrollNode)
self.scrollNode.addSubnode(self.container)
self.isReady = self.container.isReady
self.container.isReadyUpdated = { [weak self] in
guard let strongSelf = self else {
return
}
if !strongSelf.isReady {
strongSelf.isReady = true
if !strongSelf.isUpdatingState {
strongSelf.isReadyUpdated?()
}
}
}
applySmoothRoundedCorners(self.container.layer)
}
override func didLoad() {
super.didLoad()
self.scrollNode.view.alwaysBounceVertical = false
self.scrollNode.view.alwaysBounceHorizontal = false
self.scrollNode.view.bounces = false
self.scrollNode.view.showsVerticalScrollIndicator = false
self.scrollNode.view.showsHorizontalScrollIndicator = false
if #available(iOSApplicationExtension 11.0, iOS 11.0, *) {
self.scrollNode.view.contentInsetAdjustmentBehavior = .never
}
self.scrollNode.view.delaysContentTouches = false
self.scrollNode.view.clipsToBounds = false
self.scrollNode.view.delegate = self.wrappedScrollViewDelegate
self.scrollNode.view.tag = 0x5C4011
let panRecognizer = InteractiveTransitionGestureRecognizer(target: self, action: #selector(self.panGesture(_:)), allowedDirections: { [weak self] _ in
guard let strongSelf = self, !strongSelf.isDismissed else {
return []
}
return .right
})
if #available(iOS 13.4, *) {
panRecognizer.allowedScrollTypesMask = .continuous
}
self.panRecognizer = panRecognizer
if let layout = self.validLayout {
switch layout.metrics.widthClass {
case .compact:
panRecognizer.isEnabled = true
case .regular:
panRecognizer.isEnabled = false
}
}
panRecognizer.delegate = self.wrappedGestureRecognizerDelegate
panRecognizer.delaysTouchesBegan = false
panRecognizer.cancelsTouchesInView = true
if !self.isFlat {
self.view.addGestureRecognizer(panRecognizer)
self.dim.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:))))
}
}
public override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
if gestureRecognizer == self.panRecognizer, let gestureRecognizer = self.panRecognizer, gestureRecognizer.numberOfTouches == 0 {
let translation = gestureRecognizer.velocity(in: gestureRecognizer.view)
if abs(translation.y) > 4.0 && abs(translation.y) > abs(translation.x) * 2.5 {
return false
}
if translation.x < 4.0 {
return false
}
if self.isDismissed {
return false
}
return true
} else {
return true
}
}
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 func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRequireFailureOf otherGestureRecognizer: UIGestureRecognizer) -> Bool {
if let _ = otherGestureRecognizer as? InteractiveTransitionGestureRecognizer {
return true
}
return false
}
private func checkInteractiveDismissWithControllers() -> Bool {
if let controller = self.container.controllers.last {
if !controller.attemptNavigation({
}) {
return false
}
}
return true
}
@objc private func panGesture(_ recognizer: UIPanGestureRecognizer) {
switch recognizer.state {
case .began:
self.horizontalDismissOffset = 0.0
case .changed:
let translation = max(0.0, recognizer.translation(in: self.view).x)
let progress = translation / self.bounds.width
self.horizontalDismissOffset = translation
self.dismissProgress = progress
self.applyDismissProgress(transition: .immediate, completion: {})
self.container.updateAdditionalKeyboardLeftEdgeOffset(translation, transition: .immediate)
case .ended, .cancelled:
let translation = max(0.0, recognizer.translation(in: self.view).x)
let progress = translation / self.bounds.width
let velocity = recognizer.velocity(in: self.view).x
if (velocity > 1000 || progress > 0.2) && self.checkInteractiveDismissWithControllers() {
self.isDismissed = true
self.horizontalDismissOffset = self.bounds.width
self.dismissProgress = 1.0
let transition: ContainedViewLayoutTransition = .animated(duration: 0.5, curve: .spring)
transition.updateFrame(node: self.scrollNode, frame: CGRect(origin: CGPoint(x: self.bounds.width, y: 0.0), size: self.scrollNode.bounds.size))
self.container.updateAdditionalKeyboardLeftEdgeOffset(self.bounds.width, transition: transition)
self.applyDismissProgress(transition: transition, completion: { [weak self] in
guard let strongSelf = self else {
return
}
let hadInputFocus = viewTreeContainsFirstResponder(view: strongSelf.view)
strongSelf.keyboardViewManager?.dismissEditingWithoutAnimation(view: strongSelf.view)
strongSelf.interactivelyDismissed?(hadInputFocus)
})
} else {
self.horizontalDismissOffset = nil
self.dismissProgress = 0.0
let transition: ContainedViewLayoutTransition = .animated(duration: 0.1, curve: .easeInOut)
self.applyDismissProgress(transition: transition, completion: {})
self.container.updateAdditionalKeyboardLeftEdgeOffset(0.0, transition: transition)
}
default:
break
}
}
@objc func dimTapGesture(_ recognizer: UITapGestureRecognizer) {
if case .ended = recognizer.state {
if !self.isDismissed {
self.dismissWithAnimation()
}
}
}
private func dismissWithAnimation() {
let scrollView = self.scrollNode.view
let targetOffset: CGFloat
let duration = 0.3
let transition: ContainedViewLayoutTransition
let dismissProgress: CGFloat
dismissProgress = 1.0
targetOffset = 0.0
transition = .animated(duration: duration, curve: .easeInOut)
self.isDismissed = true
self.ignoreScrolling = true
let deltaY = targetOffset - scrollView.contentOffset.y
scrollView.setContentOffset(scrollView.contentOffset, animated: false)
scrollView.setContentOffset(CGPoint(x: 0.0, y: targetOffset), animated: false)
transition.animateOffsetAdditive(layer: self.scrollNode.layer, offset: -deltaY, completion: { [weak self] in
guard let strongSelf = self else {
return
}
if targetOffset == 0.0 {
strongSelf.interactivelyDismissed?(false)
}
})
self.ignoreScrolling = false
self.dismissProgress = dismissProgress
self.applyDismissProgress(transition: transition, completion: {})
self.view.endEditing(true)
}
private var isDraggingHeader = false
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if self.ignoreScrolling || self.isDismissed {
return
}
var progress = (self.bounds.height - scrollView.bounds.origin.y) / self.bounds.height
progress = max(0.0, min(1.0, progress))
self.dismissProgress = progress
self.applyDismissProgress(transition: .immediate, completion: {})
let location = scrollView.panGestureRecognizer.location(in: scrollView).offsetBy(dx: 0.0, dy: -self.container.frame.minY)
self.isDraggingHeader = location.y < 66.0
}
private func applyDismissProgress(transition: ContainedViewLayoutTransition, completion: @escaping () -> Void) {
transition.updateAlpha(node: self.dim, alpha: 1.0 - self.dismissProgress, completion: { _ in
completion()
})
self.updateDismissProgress?(self.dismissProgress, transition)
}
private var endDraggingVelocity: CGPoint?
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
let velocity = self.endDraggingVelocity ?? CGPoint()
self.endDraggingVelocity = nil
var progress = (self.bounds.height - scrollView.bounds.origin.y) / self.bounds.height
progress = max(0.0, min(1.0, progress))
let targetOffset: CGFloat
let velocityFactor: CGFloat = 0.4 / max(1.0, abs(velocity.y))
let duration = Double(min(0.3, velocityFactor))
let transition: ContainedViewLayoutTransition
let dismissProgress: CGFloat
if (velocity.y < -0.5 || progress >= 0.5) && self.checkInteractiveDismissWithControllers() {
if let controller = self.container.controllers.last as? MinimizableController, controller.isMinimizable {
dismissProgress = 0.0
targetOffset = 0.0
transition = .immediate
let topEdgeOffset = self.container.view.convert(self.container.bounds, to: self.view).minY
controller.requestMinimize(topEdgeOffset: topEdgeOffset, initialVelocity: velocity.y)
self.dim.removeFromSupernode()
} else {
dismissProgress = 1.0
targetOffset = 0.0
transition = .animated(duration: duration, curve: .easeInOut)
self.isDismissed = true
}
} else {
dismissProgress = 0.0
targetOffset = self.bounds.height
transition = .animated(duration: 0.5, curve: .spring)
}
self.ignoreScrolling = true
let deltaY = targetOffset - scrollView.contentOffset.y
scrollView.setContentOffset(scrollView.contentOffset, animated: false)
scrollView.setContentOffset(CGPoint(x: 0.0, y: targetOffset), animated: false)
transition.animateOffsetAdditive(layer: self.scrollNode.layer, offset: -deltaY, completion: { [weak self] in
guard let strongSelf = self else {
return
}
if targetOffset == 0.0 {
strongSelf.interactivelyDismissed?(false)
}
})
self.ignoreScrolling = false
self.dismissProgress = dismissProgress
self.applyDismissProgress(transition: transition, completion: {})
}
func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
self.endDraggingVelocity = velocity
targetContentOffset.pointee = scrollView.contentOffset
}
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
}
func scrollViewShouldScroll(toTop scrollView: UIScrollView) -> Bool {
return false
}
func update(layout: ContainerViewLayout, controllers: [ViewController], coveredByModalTransition: CGFloat, transition: ContainedViewLayoutTransition) {
if self.isDismissed {
return
}
self.isUpdatingState = true
self.validLayout = layout
var isStandaloneModal = false
var flatReceivesModalTransition = false
if let controller = controllers.first {
if case .standaloneModal = controller.navigationPresentation {
isStandaloneModal = true
}
if controller.flatReceivesModalTransition {
flatReceivesModalTransition = true
}
}
let _ = flatReceivesModalTransition
transition.updateFrame(node: self.dim, frame: CGRect(origin: CGPoint(), size: layout.size))
self.ignoreScrolling = true
self.scrollNode.view.isScrollEnabled = (layout.inputHeight == nil || layout.inputHeight == 0.0) && self.isInteractiveDimissEnabled && !self.isFlat
let previousBounds = self.scrollNode.bounds
let scrollNodeFrame = CGRect(origin: CGPoint(x: self.horizontalDismissOffset ?? 0.0, y: 0.0), size: layout.size)
self.scrollNode.frame = scrollNodeFrame
self.scrollNode.view.contentSize = CGSize(width: layout.size.width, height: layout.size.height * 2.0)
if !self.scrollNode.view.isDecelerating && !self.scrollNode.view.isDragging {
let defaultBounds = CGRect(origin: CGPoint(x: 0.0, y: layout.size.height), size: layout.size)
if self.scrollNode.bounds != defaultBounds {
self.scrollNode.bounds = defaultBounds
}
if previousBounds.minY != defaultBounds.minY {
transition.animateOffsetAdditive(node: self.scrollNode, offset: previousBounds.minY - defaultBounds.minY)
}
}
self.ignoreScrolling = false
self.scrollNode.view.isScrollEnabled = !isStandaloneModal && !self.isFlat
let isLandscape = layout.orientation == .landscape
let containerLayout: ContainerViewLayout
let containerFrame: CGRect
let containerScale: CGFloat
if layout.metrics.widthClass == .compact || self.isFlat {
self.panRecognizer?.isEnabled = true
self.container.clipsToBounds = true
if self.isFlat {
self.dim.backgroundColor = .clear
} else {
self.dim.backgroundColor = UIColor(white: 0.0, alpha: 0.25)
}
if isStandaloneModal || isLandscape || (self.isFlat && !flatReceivesModalTransition) {
self.container.cornerRadius = 0.0
} else {
var cornerRadius: CGFloat = 10.0
if let controller = controllers.first, controller._hasGlassStyle {
cornerRadius = 38.0
}
self.container.cornerRadius = cornerRadius
}
if #available(iOS 11.0, *) {
if layout.safeInsets.bottom.isZero {
self.container.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
} else {
self.container.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner, .layerMinXMaxYCorner, .layerMaxXMaxYCorner]
}
}
var topInset: CGFloat
if isStandaloneModal || isLandscape {
topInset = 0.0
containerLayout = layout
let unscaledFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: containerLayout.size)
containerScale = 1.0
containerFrame = unscaledFrame
} else {
topInset = 10.0
if self.isFlat {
topInset = 0.0
} else if let statusBarHeight = layout.statusBarHeight {
topInset += statusBarHeight
}
let effectiveStatusBarHeight: CGFloat?
if self.isFlat {
effectiveStatusBarHeight = layout.statusBarHeight
} else {
effectiveStatusBarHeight = nil
}
containerLayout = ContainerViewLayout(size: CGSize(width: layout.size.width, height: layout.size.height - topInset), metrics: layout.metrics, deviceMetrics: layout.deviceMetrics, intrinsicInsets: UIEdgeInsets(top: 0.0, left: layout.intrinsicInsets.left, bottom: layout.intrinsicInsets.bottom, right: layout.intrinsicInsets.right), safeInsets: UIEdgeInsets(top: 0.0, left: layout.safeInsets.left, bottom: layout.safeInsets.bottom, right: layout.safeInsets.right), additionalInsets: layout.additionalInsets, statusBarHeight: effectiveStatusBarHeight, inputHeight: layout.inputHeight, inputHeightIsInteractivellyChanging: layout.inputHeightIsInteractivellyChanging, inVoiceOver: layout.inVoiceOver)
let unscaledFrame = CGRect(origin: CGPoint(x: 0.0, y: topInset - coveredByModalTransition * 10.0), size: containerLayout.size)
let maxScale: CGFloat = (containerLayout.size.width - 16.0 * 2.0) / containerLayout.size.width
containerScale = 1.0 * (1.0 - coveredByModalTransition) + maxScale * coveredByModalTransition
var maxScaledTopInset: CGFloat = topInset - 10.0
if flatReceivesModalTransition {
maxScaledTopInset = 0.0
if let statusBarHeight = layout.statusBarHeight {
maxScaledTopInset += statusBarHeight
}
}
let scaledTopInset: CGFloat = topInset * (1.0 - coveredByModalTransition) + maxScaledTopInset * coveredByModalTransition
containerFrame = unscaledFrame.offsetBy(dx: 0.0, dy: scaledTopInset - (unscaledFrame.midY - containerScale * unscaledFrame.height / 2.0))
}
} else {
self.panRecognizer?.isEnabled = false
if self.isFlat && !flatReceivesModalTransition {
self.dim.backgroundColor = .clear
self.container.clipsToBounds = true
self.container.cornerRadius = 0.0
if #available(iOS 11.0, *) {
self.container.layer.maskedCorners = []
}
} else {
self.dim.backgroundColor = UIColor(white: 0.0, alpha: 0.4)
self.container.clipsToBounds = true
self.container.cornerRadius = 10.0
if #available(iOS 11.0, *) {
self.container.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner, .layerMinXMaxYCorner, .layerMaxXMaxYCorner]
}
}
let verticalInset: CGFloat = 44.0
let maxSide = max(layout.size.width, layout.size.height)
let minSide = min(layout.size.width, layout.size.height)
var containerSize = CGSize(width: min(layout.size.width - 20.0, floor(maxSide / 2.0)), height: min(layout.size.height, minSide) - verticalInset * 2.0)
if let preferredSize = controllers.last?.preferredContentSizeForLayout(layout) {
containerSize = preferredSize
}
containerFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - containerSize.width) / 2.0), y: floor((layout.size.height - containerSize.height) / 2.0)), size: containerSize)
containerScale = 1.0
var inputHeight: CGFloat?
if let inputHeightValue = layout.inputHeight {
inputHeight = max(0.0, inputHeightValue - (layout.size.height - containerFrame.maxY))
}
let effectiveStatusBarHeight: CGFloat?
if self.isFlat {
effectiveStatusBarHeight = layout.statusBarHeight
} else {
effectiveStatusBarHeight = nil
}
containerLayout = ContainerViewLayout(size: containerSize, metrics: layout.metrics, deviceMetrics: layout.deviceMetrics, intrinsicInsets: UIEdgeInsets(), safeInsets: UIEdgeInsets(), additionalInsets: UIEdgeInsets(), statusBarHeight: effectiveStatusBarHeight, inputHeight: inputHeight, inputHeightIsInteractivellyChanging: layout.inputHeightIsInteractivellyChanging, inVoiceOver: layout.inVoiceOver)
}
transition.updateFrameAsPositionAndBounds(node: self.container, frame: containerFrame.offsetBy(dx: 0.0, dy: layout.size.height))
transition.updateTransformScale(node: self.container, scale: containerScale)
self.container.update(layout: containerLayout, canBeClosed: true, controllers: controllers, transition: transition)
self.isUpdatingState = false
}
func animateIn(transition: ContainedViewLayoutTransition) {
if let controller = self.container.controllers.first, case .standaloneModal = controller.navigationPresentation {
} else if self.isFlat {
} else {
transition.updateAlpha(node: self.dim, alpha: 1.0)
transition.animatePositionAdditive(node: self.container, offset: CGPoint(x: 0.0, y: self.bounds.height + self.container.bounds.height / 2.0 - (self.container.position.y - self.bounds.height)))
}
}
func dismiss(transition: ContainedViewLayoutTransition, completion: @escaping () -> Void) -> ContainedViewLayoutTransition {
for controller in self.container.controllers {
controller.viewWillDisappear(transition.isAnimated)
}
if let firstController = self.container.controllers.first, case .standaloneModal = firstController.navigationPresentation {
for controller in self.container.controllers {
controller.setIgnoreAppearanceMethodInvocations(true)
controller.displayNode.removeFromSupernode()
controller.setIgnoreAppearanceMethodInvocations(false)
controller.viewDidDisappear(transition.isAnimated)
}
completion()
return transition
} else {
if transition.isAnimated && !self.isFlat {
let alphaTransition: ContainedViewLayoutTransition = .animated(duration: 0.25, curve: .easeInOut)
let positionTransition: ContainedViewLayoutTransition = .animated(duration: 0.25, curve: .easeInOut)
alphaTransition.updateAlpha(node: self.dim, alpha: 0.0, beginWithCurrentState: true)
if let lastController = self.container.controllers.last as? MinimizableController, lastController.isMinimized {
self.dim.layer.removeAllAnimations()
}
positionTransition.updatePosition(node: self.container, position: CGPoint(x: self.container.position.x, y: self.bounds.height + self.container.bounds.height / 2.0 + self.bounds.height), beginWithCurrentState: true, completion: { [weak self] _ in
guard let strongSelf = self else {
return
}
for controller in strongSelf.container.controllers {
controller.viewDidDisappear(transition.isAnimated)
}
completion()
})
return positionTransition
} else {
for controller in self.container.controllers {
controller.setIgnoreAppearanceMethodInvocations(true)
controller.displayNode.removeFromSupernode()
controller.setIgnoreAppearanceMethodInvocations(false)
controller.viewDidDisappear(transition.isAnimated)
}
completion()
return transition
}
}
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
guard let result = super.hitTest(point, with: event) else {
return nil
}
if !self.container.bounds.contains(self.view.convert(point, to: self.container.view)) {
return self.dim.view
}
if self.isFlat {
if result === self.container.view {
return nil
}
return result
}
var currentParent: UIView? = result
var enableScrolling = true
while true {
if currentParent == nil {
break
}
if currentParent is UIKeyInput {
if currentParent?.disablesInteractiveModalDismiss == true {
enableScrolling = false
break
}
} else if let scrollView = currentParent as? UIScrollView {
if scrollView === self.scrollNode.view {
break
}
if scrollView.disablesInteractiveModalDismiss {
enableScrolling = false
break
} else {
if scrollView.isDecelerating && scrollView.contentOffset.y < -scrollView.contentInset.top {
return self.scrollNode.view
}
}
} else if let listView = currentParent as? ListViewBackingView, let listNode = listView.target {
if listNode.view.disablesInteractiveModalDismiss {
enableScrolling = false
break
} else if listNode.scroller.isDecelerating && listNode.scroller.contentOffset.y < listNode.scroller.contentInset.top {
return self.scrollNode.view
}
} else if let currentParent, currentParent.disablesInteractiveModalDismiss {
enableScrolling = false
break
}
currentParent = currentParent?.superview
}
if let controller = self.container.controllers.last {
if controller.view.disablesInteractiveModalDismiss {
enableScrolling = false
}
}
self.isInteractiveDimissEnabled = enableScrolling
if let layout = self.validLayout {
if layout.inputHeight != nil && layout.inputHeight != 0.0 {
enableScrolling = false
}
}
self.scrollNode.view.isScrollEnabled = enableScrolling && !self.isFlat
return result
}
}
@@ -0,0 +1,148 @@
import Foundation
import UIKit
import AsyncDisplayKit
import SwiftSignalKit
private enum CornerType {
case topLeft
case topRight
case bottomLeft
case bottomRight
}
private func generateCornerImage(radius: CGFloat, type: CornerType) -> UIImage? {
return generateImage(CGSize(width: radius, height: radius), rotatedContext: { size, context in
context.setFillColor(UIColor.black.cgColor)
context.fill(CGRect(origin: CGPoint(), size: size))
context.setBlendMode(.copy)
context.setFillColor(UIColor.clear.cgColor)
UIGraphicsPushContext(context)
let origin: CGPoint
switch type {
case .topLeft:
origin = CGPoint()
case .topRight:
origin = CGPoint(x: -radius, y: 0.0)
case .bottomLeft:
origin = CGPoint(x: 0.0, y: -radius)
case .bottomRight:
origin = CGPoint(x: -radius, y: -radius)
}
UIBezierPath(roundedRect: CGRect(origin: origin, size: CGSize(width: radius * 2.0, height: radius * 2.0)), cornerRadius: radius).fill()
UIGraphicsPopContext()
})
}
public final class NavigationModalFrame: ASDisplayNode {
private let topShade: ASDisplayNode
private let leftShade: ASDisplayNode
private let rightShade: ASDisplayNode
private let bottomShade: ASDisplayNode
private let topLeftCorner: ASImageNode
private let topRightCorner: ASImageNode
private let bottomLeftCorner: ASImageNode
private let bottomRightCorner: ASImageNode
private var currentMaxCornerRadius: CGFloat?
private var progress: CGFloat = 1.0
private var additionalProgress: CGFloat = 0.0
private var validLayout: ContainerViewLayout?
override public init() {
self.topShade = ASDisplayNode()
self.topShade.backgroundColor = .black
self.leftShade = ASDisplayNode()
self.leftShade.backgroundColor = .black
self.rightShade = ASDisplayNode()
self.rightShade.backgroundColor = .black
self.bottomShade = ASDisplayNode()
self.bottomShade.backgroundColor = .black
self.topLeftCorner = ASImageNode()
self.topLeftCorner.displaysAsynchronously = false
self.topRightCorner = ASImageNode()
self.topRightCorner.displaysAsynchronously = false
self.bottomLeftCorner = ASImageNode()
self.bottomLeftCorner.displaysAsynchronously = false
self.bottomRightCorner = ASImageNode()
self.bottomRightCorner.displaysAsynchronously = false
super.init()
self.addSubnode(self.topShade)
self.addSubnode(self.leftShade)
self.addSubnode(self.rightShade)
self.addSubnode(self.bottomShade)
self.addSubnode(self.topLeftCorner)
self.addSubnode(self.topRightCorner)
self.addSubnode(self.bottomLeftCorner)
self.addSubnode(self.bottomRightCorner)
}
public func update(layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
self.validLayout = layout
self.updateShades(layout: layout, progress: 1.0 - self.progress, additionalProgress: self.additionalProgress, transition: transition, completion: {})
}
public func updateDismissal(transition: ContainedViewLayoutTransition, progress: CGFloat, additionalProgress: CGFloat, completion: @escaping () -> Void) {
self.progress = progress
self.additionalProgress = additionalProgress
if let layout = self.validLayout {
self.updateShades(layout: layout, progress: 1.0 - progress, additionalProgress: additionalProgress, transition: transition, completion: completion)
} else {
completion()
}
}
private func updateShades(layout: ContainerViewLayout, progress: CGFloat, additionalProgress: CGFloat, transition: ContainedViewLayoutTransition, completion: @escaping () -> Void) {
let sideInset: CGFloat = 16.0
var topInset: CGFloat = 0.0
if let statusBarHeight = layout.statusBarHeight {
topInset += statusBarHeight
}
let additionalTopInset: CGFloat = 10.0
let contentScale = (layout.size.width - sideInset * 2.0) / layout.size.width
let bottomInset: CGFloat = layout.size.height - contentScale * layout.size.height - topInset
let cornerRadius: CGFloat = 28.0
let initialCornerRadius: CGFloat
if !layout.safeInsets.top.isZero {
initialCornerRadius = layout.deviceMetrics.screenCornerRadius
} else {
initialCornerRadius = 0.0
}
if self.currentMaxCornerRadius != cornerRadius {
self.topLeftCorner.image = generateCornerImage(radius: max(initialCornerRadius, cornerRadius), type: .topLeft)
self.topRightCorner.image = generateCornerImage(radius: max(initialCornerRadius, cornerRadius), type: .topRight)
self.bottomLeftCorner.image = generateCornerImage(radius: max(initialCornerRadius, cornerRadius), type: .bottomLeft)
self.bottomRightCorner.image = generateCornerImage(radius: max(initialCornerRadius, cornerRadius), type: .bottomRight)
}
let cornerSize = progress * cornerRadius + (1.0 - progress) * initialCornerRadius
let cornerSideOffset: CGFloat = progress * sideInset + additionalProgress * sideInset
let cornerTopOffset: CGFloat = progress * topInset + additionalProgress * additionalTopInset
let cornerBottomOffset: CGFloat = progress * bottomInset
transition.updateFrame(node: self.topLeftCorner, frame: CGRect(origin: CGPoint(x: cornerSideOffset, y: cornerTopOffset), size: CGSize(width: cornerSize, height: cornerSize)), beginWithCurrentState: true)
transition.updateFrame(node: self.topRightCorner, frame: CGRect(origin: CGPoint(x: layout.size.width - cornerSideOffset - cornerSize, y: cornerTopOffset), size: CGSize(width: cornerSize, height: cornerSize)), beginWithCurrentState: true)
transition.updateFrame(node: self.bottomLeftCorner, frame: CGRect(origin: CGPoint(x: cornerSideOffset, y: layout.size.height - cornerBottomOffset - cornerSize), size: CGSize(width: cornerSize, height: cornerSize)), beginWithCurrentState: true)
transition.updateFrame(node: self.bottomRightCorner, frame: CGRect(origin: CGPoint(x: layout.size.width - cornerSideOffset - cornerSize, y: layout.size.height - cornerBottomOffset - cornerSize), size: CGSize(width: cornerSize, height: cornerSize)), beginWithCurrentState: true)
let topShadeOffset: CGFloat = progress * topInset + additionalProgress * additionalTopInset
let bottomShadeOffset: CGFloat = progress * bottomInset
let leftShadeOffset: CGFloat = progress * sideInset + additionalProgress * sideInset
let rightShadeWidth: CGFloat = progress * sideInset + additionalProgress * sideInset
let rightShadeOffset: CGFloat = layout.size.width - rightShadeWidth
transition.updateFrame(node: self.topShade, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: layout.size.width, height: topShadeOffset)), beginWithCurrentState: true)
transition.updateFrame(node: self.bottomShade, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - bottomShadeOffset), size: CGSize(width: layout.size.width, height: bottomShadeOffset)))
transition.updateFrame(node: self.leftShade, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: leftShadeOffset, height: layout.size.height)), beginWithCurrentState: true)
transition.updateFrame(node: self.rightShade, frame: CGRect(origin: CGPoint(x: rightShadeOffset, y: 0.0), size: CGSize(width: rightShadeWidth, height: layout.size.height)), beginWithCurrentState: true, completion: { _ in
completion()
})
}
}
@@ -0,0 +1,100 @@
import Foundation
import UIKit
import AsyncDisplayKit
import SwiftSignalKit
final class NavigationOverlayContainer: ASDisplayNode {
let controller: ViewController
let blocksInteractionUntilReady: Bool
private(set) var isReady: Bool = false
var isRemoved: Bool = false
var isReadyUpdated: (() -> Void)?
private var isReadyDisposable: Disposable?
private var validLayout: ContainerViewLayout?
private var isUpdatingState: Bool = false
var keyboardViewManager: KeyboardViewManager? {
didSet {
if self.keyboardViewManager !== oldValue {
}
}
}
init(controller: ViewController, blocksInteractionUntilReady: Bool, controllerRemoved: @escaping (ViewController) -> Void, statusBarUpdated: @escaping (ContainedViewLayoutTransition) -> Void, modalStyleOverlayTransitionFactorUpdated: @escaping (ContainedViewLayoutTransition) -> Void) {
self.controller = controller
self.blocksInteractionUntilReady = blocksInteractionUntilReady
super.init()
self.controller.navigation_setDismiss({ [weak self] in
guard let strongSelf = self else {
return
}
controllerRemoved(strongSelf.controller)
}, rootController: nil)
self.controller.statusBar.alphaUpdated = { transition in
statusBarUpdated(transition)
}
self.controller.modalStyleOverlayTransitionFactorUpdated = { transition in
modalStyleOverlayTransitionFactorUpdated(transition)
}
self.isReadyDisposable = (self.controller.ready.get()
|> filter { $0 }
|> take(1)
|> deliverOnMainQueue).start(next: { [weak self] _ in
guard let strongSelf = self else {
return
}
if !strongSelf.isReady {
strongSelf.isReady = true
if !strongSelf.isUpdatingState {
strongSelf.isReadyUpdated?()
}
}
}).strict()
}
deinit {
self.isReadyDisposable?.dispose()
}
override func didLoad() {
super.didLoad()
}
func update(layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
self.isUpdatingState = true
let updateLayout = self.validLayout != layout
self.validLayout = layout
if updateLayout {
transition.updateFrame(node: self.controller.displayNode, frame: CGRect(origin: CGPoint(), size: layout.size))
self.controller.containerLayoutUpdated(layout, transition: transition)
}
self.isUpdatingState = false
}
func transitionIn() {
self.controller.viewWillAppear(false)
self.controller.setIgnoreAppearanceMethodInvocations(true)
self.addSubnode(self.controller.displayNode)
self.controller.setIgnoreAppearanceMethodInvocations(false)
self.controller.viewDidAppear(false)
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if let result = self.controller.view.hitTest(point, with: event) {
return result
}
return nil
}
}
@@ -0,0 +1,130 @@
import Foundation
import UIKit
import AsyncDisplayKit
import SwiftSignalKit
enum NavigationSplitContainerScrollToTop {
case master
case detail
}
final class NavigationSplitContainer: ASDisplayNode {
private var theme: NavigationControllerTheme
private let masterScrollToTopView: ScrollToTopView
private let detailScrollToTopView: ScrollToTopView
private let masterContainer: NavigationContainer
private let detailContainer: NavigationContainer
private let separator: ASDisplayNode
private(set) var masterControllers: [ViewController] = []
private(set) var detailControllers: [ViewController] = []
var canHaveKeyboardFocus: Bool = false {
didSet {
self.masterContainer.canHaveKeyboardFocus = self.canHaveKeyboardFocus
self.detailContainer.canHaveKeyboardFocus = self.canHaveKeyboardFocus
}
}
var isInFocus: Bool = false {
didSet {
if self.isInFocus != oldValue {
self.inFocusUpdated(isInFocus: self.isInFocus)
}
}
}
func inFocusUpdated(isInFocus: Bool) {
self.masterContainer.isInFocus = isInFocus
self.detailContainer.isInFocus = isInFocus
}
init(theme: NavigationControllerTheme, controllerRemoved: @escaping (ViewController) -> Void, scrollToTop: @escaping (NavigationSplitContainerScrollToTop) -> Void) {
self.theme = theme
self.masterScrollToTopView = ScrollToTopView(frame: CGRect())
self.masterScrollToTopView.action = {
scrollToTop(.master)
}
self.detailScrollToTopView = ScrollToTopView(frame: CGRect())
self.detailScrollToTopView.action = {
scrollToTop(.detail)
}
self.masterContainer = NavigationContainer(isFlat: false, controllerRemoved: controllerRemoved)
self.masterContainer.clipsToBounds = true
self.detailContainer = NavigationContainer(isFlat: false, controllerRemoved: controllerRemoved)
self.detailContainer.clipsToBounds = true
self.separator = ASDisplayNode()
self.separator.backgroundColor = theme.navigationBar.separatorColor
super.init()
self.addSubnode(self.masterContainer)
self.addSubnode(self.detailContainer)
self.addSubnode(self.separator)
self.view.addSubview(self.masterScrollToTopView)
self.view.addSubview(self.detailScrollToTopView)
}
func hasNonReadyControllers() -> Bool {
if self.masterContainer.hasNonReadyControllers() {
return true
}
if self.detailContainer.hasNonReadyControllers() {
return true
}
return false
}
func updateTheme(theme: NavigationControllerTheme) {
self.separator.backgroundColor = theme.navigationBar.separatorColor
}
func update(layout: ContainerViewLayout, masterControllers: [ViewController], detailControllers: [ViewController], detailsPlaceholderNode: NavigationDetailsPlaceholderNode?, transition: ContainedViewLayoutTransition) {
let masterWidth: CGFloat = min(max(320.0, floor(layout.size.width / 3.0)), floor(layout.size.width / 2.0))
let detailWidth = layout.size.width - masterWidth
self.masterScrollToTopView.frame = CGRect(origin: CGPoint(x: 0.0, y: -1.0), size: CGSize(width: masterWidth, height: 1.0))
self.detailScrollToTopView.frame = CGRect(origin: CGPoint(x: masterWidth, y: -1.0), size: CGSize(width: detailWidth, height: 1.0))
transition.updateFrame(node: self.masterContainer, frame: CGRect(origin: CGPoint(), size: CGSize(width: masterWidth, height: layout.size.height)))
transition.updateFrame(node: self.detailContainer, frame: CGRect(origin: CGPoint(x: masterWidth, y: 0.0), size: CGSize(width: detailWidth, height: layout.size.height)))
transition.updateFrame(node: self.separator, frame: CGRect(origin: CGPoint(x: masterWidth, y: 0.0), size: CGSize(width: UIScreenPixel, height: layout.size.height)))
if let detailsPlaceholderNode {
let needsTiling = layout.size.width > layout.size.height
detailsPlaceholderNode.updateLayout(size: CGSize(width: detailWidth, height: layout.size.height), needsTiling: needsTiling, transition: transition)
transition.updateFrame(node: detailsPlaceholderNode, frame: CGRect(origin: CGPoint(x: masterWidth, y: 0.0), size: CGSize(width: detailWidth, height: layout.size.height)))
}
self.masterContainer.update(layout: ContainerViewLayout(size: CGSize(width: masterWidth, height: layout.size.height), metrics: layout.metrics, deviceMetrics: layout.deviceMetrics, intrinsicInsets: layout.intrinsicInsets, safeInsets: layout.safeInsets, additionalInsets: UIEdgeInsets(), statusBarHeight: layout.statusBarHeight, inputHeight: layout.inputHeight, inputHeightIsInteractivellyChanging: layout.inputHeightIsInteractivellyChanging, inVoiceOver: layout.inVoiceOver), canBeClosed: false, controllers: masterControllers, transition: transition)
self.detailContainer.update(layout: ContainerViewLayout(size: CGSize(width: detailWidth, height: layout.size.height), metrics: layout.metrics, deviceMetrics: layout.deviceMetrics, intrinsicInsets: layout.intrinsicInsets, safeInsets: layout.safeInsets, additionalInsets: layout.additionalInsets, statusBarHeight: layout.statusBarHeight, inputHeight: layout.inputHeight, inputHeightIsInteractivellyChanging: layout.inputHeightIsInteractivellyChanging, inVoiceOver: layout.inVoiceOver), canBeClosed: true, controllers: detailControllers, transition: transition)
var controllersUpdated = false
if self.detailControllers.last !== detailControllers.last {
controllersUpdated = true
} else if self.masterControllers.count != masterControllers.count {
controllersUpdated = true
} else {
for i in 0 ..< masterControllers.count {
if masterControllers[i] !== self.masterControllers[i] {
controllersUpdated = true
break
}
}
}
self.masterControllers = masterControllers
self.detailControllers = detailControllers
if controllersUpdated {
let data = self.detailControllers.last?.customData
for controller in self.masterControllers {
controller.updateNavigationCustomData(data, progress: 1.0, transition: transition)
}
}
}
}