import UIKit import AppBundle import AsyncDisplayKit enum NavigationTransition { case Push case Pop } private let shadowWidth: CGFloat = 16.0 private func generateShadow() -> UIImage? { return generateImage(CGSize(width: 16.0, height: 1.0), rotatedContext: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) context.setFillColor(UIColor.black.cgColor) context.setShadow(offset: CGSize(), blur: 16.0, color: UIColor(white: 0.0, alpha: 0.5).cgColor) context.fill(CGRect(origin: CGPoint(x: size.width, y: 0.0), size: CGSize(width: 16.0, height: 1.0))) }) } private let shadowImage = generateShadow() public protocol CustomNavigationTransitionNode: ASDisplayNode { func setup(topNavigationBar: NavigationBar, bottomNavigationBar: NavigationBar) func update(containerSize: CGSize, fraction: CGFloat, transition: ContainedViewLayoutTransition) func restore() } final class NavigationTransitionCoordinator { private var _progress: CGFloat = 0.0 var progress: CGFloat { get { return self._progress } } private let container: NavigationContainer private let transition: NavigationTransition let isInteractive: Bool let isFlat: Bool let topNode: ASDisplayNode let bottomNode: ASDisplayNode private let topNavigationBar: NavigationBar? private let bottomNavigationBar: NavigationBar? private let dimNode: ASDisplayNode private let shadowNode: ASImageNode private let customTransitionNode: CustomNavigationTransitionNode? private let inlineNavigationBarTransition: Bool private(set) var animatingCompletion = false private var currentCompletion: (() -> Void)? private var didUpdateProgress: ((CGFloat, ContainedViewLayoutTransition, CGRect, CGRect) -> Void)? private var frameRateLink: SharedDisplayLinkDriver.Link? init(transition: NavigationTransition, isInteractive: Bool, isFlat: Bool, container: NavigationContainer, topNode: ASDisplayNode, topNavigationBar: NavigationBar?, bottomNode: ASDisplayNode, bottomNavigationBar: NavigationBar?, didUpdateProgress: ((CGFloat, ContainedViewLayoutTransition, CGRect, CGRect) -> Void)? = nil) { self.transition = transition self.isInteractive = isInteractive self.isFlat = isFlat self.container = container self.didUpdateProgress = didUpdateProgress self.topNode = topNode self.bottomNode = bottomNode self.topNavigationBar = topNavigationBar self.bottomNavigationBar = bottomNavigationBar self.dimNode = ASDisplayNode() self.dimNode.backgroundColor = UIColor.black self.shadowNode = ASImageNode() self.shadowNode.displaysAsynchronously = false self.shadowNode.image = shadowImage if let topNavigationBar = topNavigationBar, let bottomNavigationBar = bottomNavigationBar { if let customTransitionNode = topNavigationBar.makeCustomTransitionNode?(bottomNavigationBar, isInteractive) { self.inlineNavigationBarTransition = false customTransitionNode.setup(topNavigationBar: topNavigationBar, bottomNavigationBar: bottomNavigationBar) self.customTransitionNode = customTransitionNode } else if let customTransitionNode = bottomNavigationBar.makeCustomTransitionNode?(topNavigationBar, isInteractive) { self.inlineNavigationBarTransition = false customTransitionNode.setup(topNavigationBar: topNavigationBar, bottomNavigationBar: bottomNavigationBar) self.customTransitionNode = customTransitionNode } else if !topNavigationBar.isHidden, !bottomNavigationBar.isHidden, topNavigationBar.canTransitionInline, bottomNavigationBar.canTransitionInline, topNavigationBar.item?.leftBarButtonItem == nil { var topFrame = topNavigationBar.view.convert(topNavigationBar.bounds, to: container.view) var bottomFrame = bottomNavigationBar.view.convert(bottomNavigationBar.bounds, to: container.view) topFrame.origin.x = 0.0 bottomFrame.origin.x = 0.0 self.inlineNavigationBarTransition = true self.customTransitionNode = nil } else { self.inlineNavigationBarTransition = false self.customTransitionNode = nil } } else { self.inlineNavigationBarTransition = false self.customTransitionNode = nil } switch transition { case .Push: self.container.addSubnode(topNode) case .Pop: if topNode.supernode == self.container { self.container.insertSubnode(bottomNode, belowSubnode: topNode) } else { self.container.addSubnode(topNode) } } if !self.isFlat { self.container.insertSubnode(self.dimNode, belowSubnode: topNode) self.container.insertSubnode(self.shadowNode, belowSubnode: self.dimNode) } if let customTransitionNode = self.customTransitionNode { self.container.addSubnode(customTransitionNode) } self.maybeCreateNavigationBarTransition() self.updateProgress(0.0, transition: .immediate, completion: {}) self.frameRateLink = SharedDisplayLinkDriver.shared.add(framesPerSecond: .max, { _ in }) } required init(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } func updateProgress(_ progress: CGFloat, transition: ContainedViewLayoutTransition, completion: @escaping () -> Void) { self._progress = progress let position: CGFloat switch self.transition { case .Push: position = 1.0 - progress case .Pop: position = progress } var dimInset: CGFloat = 0.0 if let bottomNavigationBar = self.bottomNavigationBar , self.inlineNavigationBarTransition { if self.bottomNavigationBar?.isBackgroundVisible == false || self.topNavigationBar?.isBackgroundVisible == false { } else { dimInset = bottomNavigationBar.frame.maxY } } let containerSize = self.container.bounds.size let topFrame = CGRect(origin: CGPoint(x: floorToScreenPixels(position * containerSize.width), y: 0.0), size: containerSize) let bottomFrame = CGRect(origin: CGPoint(x: self.isFlat ? -floorToScreenPixels((1.0 - position) * containerSize.width) : ((position - 1.0) * containerSize.width * 0.3), y: 0.0), size: containerSize) var canInvokeCompletion = false var hadEarlyCompletion = false transition.updateFrame(node: self.topNode, frame: topFrame, completion: { _ in if canInvokeCompletion { completion() } else { hadEarlyCompletion = true } }) canInvokeCompletion = true transition.updateFrame(node: self.dimNode, frame: CGRect(origin: CGPoint(x: 0.0, y: dimInset), size: CGSize(width: max(0.0, topFrame.minX + self.container.overflowInset), height: self.container.bounds.size.height - dimInset))) transition.updateFrame(node: self.shadowNode, frame: CGRect(origin: CGPoint(x: self.dimNode.frame.maxX - shadowWidth, y: dimInset), size: CGSize(width: shadowWidth, height: containerSize.height - dimInset))) transition.updateAlpha(node: self.dimNode, alpha: (1.0 - position) * 0.15) transition.updateAlpha(node: self.shadowNode, alpha: (1.0 - position) * 0.9) transition.updateFrame(node: self.bottomNode, frame: bottomFrame) self.updateNavigationBarTransition(transition: transition) if let customTransitionNode = self.customTransitionNode { customTransitionNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: containerSize.width, height: containerSize.height)) customTransitionNode.update(containerSize: containerSize, fraction: position, transition: transition) } self.didUpdateProgress?(self.progress, transition, topFrame, bottomFrame) if hadEarlyCompletion { completion() } } private func updateNavigationBarTransition(transition: ContainedViewLayoutTransition) { if let topNavigationBar = self.topNavigationBar, let bottomNavigationBar = self.bottomNavigationBar, self.inlineNavigationBarTransition { let position: CGFloat switch self.transition { case .Push: position = 1.0 - progress case .Pop: position = progress } transition.animateView { topNavigationBar.transitionState = NavigationBarTransitionState(navigationBar: bottomNavigationBar, transition: self.transition, role: .top, progress: position) bottomNavigationBar.transitionState = NavigationBarTransitionState(navigationBar: topNavigationBar, transition: self.transition, role: .bottom, progress: position) } } } func maybeCreateNavigationBarTransition() { if let topNavigationBar = self.topNavigationBar, let bottomNavigationBar = self.bottomNavigationBar, self.inlineNavigationBarTransition { let position: CGFloat switch self.transition { case .Push: position = 1.0 - progress case .Pop: position = progress } topNavigationBar.transitionState = NavigationBarTransitionState(navigationBar: bottomNavigationBar, transition: self.transition, role: .top, progress: position) bottomNavigationBar.transitionState = NavigationBarTransitionState(navigationBar: topNavigationBar, transition: self.transition, role: .bottom, progress: position) } } func endNavigationBarTransition() { if let topNavigationBar = self.topNavigationBar, let bottomNavigationBar = self.bottomNavigationBar, self.inlineNavigationBarTransition { topNavigationBar.transitionState = nil bottomNavigationBar.transitionState = nil } } func animateCancel(_ completion: @escaping () -> ()) { self.currentCompletion = completion self.updateProgress(0.0, transition: .animated(duration: 0.1, curve: .easeInOut), completion: { [weak self] in guard let strongSelf = self else { return } switch strongSelf.transition { case .Push: strongSelf.topNode.removeFromSupernode() case .Pop: strongSelf.bottomNode.removeFromSupernode() } strongSelf.dimNode.removeFromSupernode() strongSelf.shadowNode.removeFromSupernode() strongSelf.customTransitionNode?.restore() strongSelf.customTransitionNode?.removeFromSupernode() strongSelf.endNavigationBarTransition() if let currentCompletion = strongSelf.currentCompletion { strongSelf.currentCompletion = nil currentCompletion() } }) } func complete() { self.animatingCompletion = true self._progress = 1.0 self.dimNode.removeFromSupernode() self.shadowNode.removeFromSupernode() self.customTransitionNode?.restore() self.customTransitionNode?.removeFromSupernode() self.endNavigationBarTransition() if let currentCompletion = self.currentCompletion { self.currentCompletion = nil currentCompletion() } } func performCompletion(completion: @escaping () -> ()) { self.updateProgress(1.0, transition: .immediate, completion: { [weak self] in if let strongSelf = self { strongSelf.dimNode.removeFromSupernode() strongSelf.shadowNode.removeFromSupernode() strongSelf.customTransitionNode?.restore() strongSelf.customTransitionNode?.removeFromSupernode() strongSelf.endNavigationBarTransition() if let currentCompletion = strongSelf.currentCompletion { strongSelf.currentCompletion = nil currentCompletion() } } completion() }) } func animateCompletion(_ velocity: CGFloat, completion: @escaping () -> ()) { self.animatingCompletion = true let distance = (1.0 - self.progress) * self.container.bounds.size.width self.currentCompletion = completion let f = { self.dimNode.removeFromSupernode() self.shadowNode.removeFromSupernode() self.customTransitionNode?.restore() self.customTransitionNode?.removeFromSupernode() self.endNavigationBarTransition() if let currentCompletion = self.currentCompletion { self.currentCompletion = nil currentCompletion() } } if abs(velocity) < CGFloat.ulpOfOne && abs(self.progress) < CGFloat.ulpOfOne { self.updateProgress(1.0, transition: .animated(duration: 0.5, curve: .spring), completion: { f() }) } else { self.updateProgress(1.0, transition: .animated(duration: Double(max(0.05, min(0.2, abs(distance / velocity)))), curve: .easeInOut), completion: { f() }) } } }