Files
2022-05-31 11:44:27 +00:00

258 lines
10 KiB
Swift
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//
// 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 receivers 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 receivers 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)..<self.viewControllers.count
self.viewControllers.removeSubrange(range)
}
self.animatePop(toView: viewController.view, animation: animation)
CATransaction.commit()
} else {
topViewController.view.removeFromSuperview()
let range = (viewControllerPosition! + 1)..<self.viewControllers.count
self.viewControllers.removeSubrange(range)
}
}
func pop(toViewController viewController: NSViewController, animated: Bool) {
if animated {
self.pop(toViewController: viewController, animation: self.defaultPopAnimation())
} else {
self.pop(toViewController: viewController, animation: nil)
}
}
func popToRootViewController(animation: AnimationBlock?) {
guard let rootViewController = self.viewControllers.first
, let topViewController = self.topViewController
, topViewController != rootViewController else {
return
}
self.pop(toViewController: rootViewController, animation: animation)
}
func popToRootViewController(animated: Bool, moveContentViewBack: Bool = false) {
if animated {
self.popToRootViewController(animation: self.defaultPopAnimation())
} else {
self.popToRootViewController(animation: nil)
}
}
// MARK: - Animating
private func animate(fromView: NSView?, toView: NSView?, animation: AnimationBlock) {
fromView?.wantsLayer = true
toView?.wantsLayer = true
animation(fromView, toView).fromViewAnimations.forEach {
fromView?.layer?.add($0, forKey: nil)
}
animation(fromView, toView).toViewAnimations.forEach {
toView?.layer?.add($0, forKey: nil)
}
}
func animatePush(_ animation: AnimationBlock) {
let fromView = self.previousViewController?.view
let toView = self.topViewController?.view
self.animate(fromView: fromView, toView: toView, animation: animation)
}
func animatePop(toView view: NSView?, animation: AnimationBlock) {
let fromView = self.topViewController?.view
let toView = view
self.animate(fromView: fromView, toView: toView, animation: animation)
}
}