// // JSViewControllersStackController.swift // JSNavigationController // import AppKit typealias AnimationBlock = (_ fromView: NSView?, _ toView: NSView?) -> (fromViewAnimations: [CAAnimation], toViewAnimations: [CAAnimation]) protocol JSViewControllersStackManager: AnyObject { /// The view in which views will be pushed. var contentView: NSView? { get set } /// The view controllers currently on the navigation stack. var viewControllers: [NSViewController] { get set } /// The view controller at the top of the navigation stack. var topViewController: NSViewController? { get } /// The view controller above the top view controller. Nil if the top view controller is the root view controller. var previousViewController: NSViewController? { get } /** Replaces the view controllers currently managed by the navigation controller with the specified items. - parameter viewControllers: The view controllers to place in the stack. The front-to-back order of the controllers in this array represents the new bottom-to-top order of the controllers in the navigation stack. Thus, the last item added to the array becomes the top item of the navigation stack. - parameter animated: If true, animate the pushing or popping of the top view controller. If false, replace the view controllers without any animations. */ func set(viewControllers: [NSViewController], animated: Bool) // MARK: - Pushing /** Pushes a view controller onto the receiver’s stack and updates the display. - parameter viewController: The view controller to push onto the stack. If the view controller is already on the navigation stack, this method does nothing. - parameter animation: The animation block to apply during the transition. Specify nil if you do not want the transition to be animated. */ func push(viewController: NSViewController, animation: AnimationBlock?) /** Pushes a view controller onto the receiver’s stack and updates the display. - parameter viewController: The view controller to push onto the stack. If the view controller is already on the navigation stack, this method does nothing. - parameter animated: Specify true to animate the transition or false if you do not want the transition to be animated. You might specify false if you are setting up the navigation controller at launch time.. */ func push(viewController: NSViewController, animated: Bool, offsetContentView: Bool) // MARK: - Popping /** Pops the top view controller from the navigation stack and updates the display. - parameter animation: The animation block to apply during the transition. Specify nil if you do not want the transition to be animated. */ func popViewController(animation: AnimationBlock?) /** Pops the top view controller from the navigation stack and updates the display. - parameter animated: Specify true to animate the transition or false if you do not want the transition to be animated. */ func popViewController(animated: Bool) /** Pops view controllers until the specified view controller is at the top of the navigation stack. - parameter viewController: The view controller that you want to be at the top of the stack. Does nothing if this view controller is not on the navigation stack. - parameter animation: The animation block to apply during the transition. Specify nil if you do not want the transition to be animated. */ func pop(toViewController viewController: NSViewController, animation: AnimationBlock?) /** Pops view controllers until the specified view controller is at the top of the navigation stack. - parameter viewController: The view controller that you want to be at the top of the stack. - parameter animated: Specify true to animate the transition or false if you do not want the transition to be animated. */ func pop(toViewController viewController: NSViewController, animated: Bool) /** Pops all the view controllers on the stack except the root view controller and updates the display. - parameter animation: The animation block to apply during the transition. Specify nil if you do not want the transition to be animated. */ func popToRootViewController(animation: AnimationBlock?) /** Pops all the view controllers on the stack except the root view controller and updates the display. - parameter animated: Specify true to animate the transition or false if you do not want the transition to be animated. */ func popToRootViewController(animated: Bool, moveContentViewBack: Bool) // MARK: - Animating func animatePush(_ animation: AnimationBlock) func animatePop(toView view: NSView?, animation: AnimationBlock) func defaultPushAnimation() -> AnimationBlock func defaultPopAnimation() -> AnimationBlock } // MARK: - extension JSViewControllersStackManager { var topViewController: NSViewController? { return self.viewControllers.last } var previousViewController: NSViewController? { return self.viewControllers[safe: self.viewControllers.count - 2] } func set(viewControllers: [NSViewController], animated: Bool) { guard !viewControllers.isEmpty else { return } if animated { if let lastViewController = viewControllers.last { if self.viewControllers.contains(lastViewController) && lastViewController != self.topViewController { self.pop(toViewController: lastViewController, animated: true) } else { self.push(viewController: lastViewController, animated: true) } } } else { if let lastViewController = viewControllers.last { self.push(viewController: lastViewController, animated: false) } self.viewControllers = viewControllers } } func push(viewController: NSViewController, animation: AnimationBlock?) { guard !Set(self.viewControllers).contains(viewController) else { return } self.viewControllers.append(viewController) // Remove old view if let previousViewController = self.previousViewController , !animation.isExist { previousViewController.view.removeFromSuperview() } // Add the new view self.contentView?.addSubview(viewController.view, positioned: .above, relativeTo: self.previousViewController?.view) if let animation = animation { CATransaction.begin() CATransaction.setCompletionBlock { [weak self] in self?.previousViewController?.view.removeFromSuperview() self?.previousViewController?.view.layer?.removeAllAnimations() } self.animatePush(animation) CATransaction.commit() } } func push(viewController: NSViewController, animated: Bool, offsetContentView: Bool = false) { if animated { self.push(viewController: viewController, animation: self.defaultPushAnimation()) } else { self.push(viewController: viewController, animation: nil) } } // MARK: - Popping func popViewController(animation: AnimationBlock?) { guard let previousViewController = self.previousViewController else { return } // You can't pop the root view controller self.pop(toViewController: previousViewController, animation: animation) } func popViewController(animated: Bool) { if animated { self.popViewController(animation: self.defaultPopAnimation()) } else { self.popViewController(animation: nil) } } func pop(toViewController viewController: NSViewController, animation: AnimationBlock?) { guard Set(self.viewControllers).contains(viewController) , let rootViewController = self.viewControllers.first , let topViewController = self.topViewController , topViewController != rootViewController else { return } let viewControllerPosition = self.viewControllers.firstIndex(of: viewController) // Add the new view self.contentView?.addSubview(viewController.view, positioned: .below, relativeTo: topViewController.view) if let animation = animation { CATransaction.begin() CATransaction.setCompletionBlock { [unowned self] in self.topViewController?.view.removeFromSuperview() self.topViewController?.view.layer?.removeAllAnimations() let range = (viewControllerPosition! + 1)..