296 lines
13 KiB
Swift
296 lines
13 KiB
Swift
//
|
|
// JSNavigationController.swift
|
|
// JSNavigationController
|
|
//
|
|
|
|
import AppKit
|
|
|
|
protocol JSNavigationControllerViewDelegate: AnyObject {
|
|
|
|
var contentView: NSView { get }
|
|
var navigationBarView: NSView? { get }
|
|
var leadingConstraint: NSLayoutConstraint? { get set}
|
|
|
|
func hasNavigationBar(_ hasBar: Bool)
|
|
}
|
|
|
|
final class JSNavigationController: NSViewController, JSViewControllersStackManager {
|
|
|
|
private struct Constants {
|
|
static let segueTemplatesKey = "segueTemplates"
|
|
static let segueIdentifierKey = "identifier"
|
|
static let segueIdentifier = "rootViewController"
|
|
}
|
|
|
|
weak var controllerViewDelegate: JSNavigationControllerViewDelegate?
|
|
|
|
weak var contentView: NSView?
|
|
var viewControllers: [NSViewController] = []
|
|
var navigationBarController: JSNavigationBarController?
|
|
weak var delegate: JSNavigationControllerDelegate?
|
|
|
|
// MARK: - Creating Navigation Controllers
|
|
init(rootViewController: NSViewController, viewDelegate: JSNavigationControllerViewDelegate) {
|
|
self.contentView = viewDelegate.contentView
|
|
self.navigationBarController = JSNavigationBarController(view: viewDelegate.navigationBarView)
|
|
self.controllerViewDelegate = viewDelegate
|
|
super.init(nibName: nil, bundle: nil)
|
|
self.push(viewController: rootViewController, animated: false)
|
|
}
|
|
|
|
init(viewControllers: [NSViewController], viewDelegate: JSNavigationControllerViewDelegate) {
|
|
self.contentView = viewDelegate.contentView
|
|
self.navigationBarController = JSNavigationBarController(view: viewDelegate.navigationBarView)
|
|
self.controllerViewDelegate = viewDelegate
|
|
super.init(nibName: nil, bundle: nil)
|
|
self.set(viewControllers: viewControllers, animated: false)
|
|
}
|
|
|
|
required public init?(coder: NSCoder) {
|
|
super.init(coder: coder)
|
|
}
|
|
|
|
// MARK: - View Lifecycle
|
|
override func loadView() {
|
|
if self.nibName.isExist {
|
|
super.loadView()
|
|
} else {
|
|
let v = NSView(frame: .zero)
|
|
v.translatesAutoresizingMaskIntoConstraints = false
|
|
v.wantsLayer = true
|
|
self.view = v
|
|
}
|
|
}
|
|
|
|
override func viewDidAppear() {
|
|
super.viewDidAppear()
|
|
|
|
guard self.nibName != nil else { return }
|
|
guard let segues = self.value(forKey: Constants.segueTemplatesKey) as? [NSObject] else { return } // Undocumented
|
|
|
|
for segue in segues {
|
|
if let id = segue.value(forKey: Constants.segueIdentifierKey) as? String {
|
|
self.performSegue(withIdentifier: id, sender: self)
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Pushing
|
|
func push(viewController: NSViewController, contentAnimation: AnimationBlock?, navigationBarAnimation: AnimationBlock?) {
|
|
guard !Set(self.viewControllers).contains(viewController) else { return }
|
|
|
|
self.viewControllers.append(viewController)
|
|
self.delegate?.navigationController(self, willShowViewController: viewController, animated: (contentAnimation != nil))
|
|
|
|
// Remove old view
|
|
if let previousViewController = self.previousViewController
|
|
, contentAnimation == nil {
|
|
previousViewController.view.removeFromSuperview()
|
|
}
|
|
|
|
// Add the new view
|
|
self.contentView?.addSubview(viewController.view, positioned: .above, relativeTo: self.previousViewController?.view)
|
|
|
|
// NavigationBar
|
|
if let vc = viewController as? JSNavigationBarViewControllerProvider {
|
|
vc.navigationController = self
|
|
self.navigationBarController?.push(viewController: vc.navigationBarViewController(), animation: navigationBarAnimation)
|
|
} else {
|
|
self.navigationBarController?.push(viewController: EmptyViewController(), animation: navigationBarAnimation)
|
|
}
|
|
|
|
self.controllerViewDelegate?.hasNavigationBar(viewController is JSNavigationBarViewControllerProvider)
|
|
|
|
if let contentAnimation = contentAnimation {
|
|
CATransaction.begin()
|
|
CATransaction.setCompletionBlock { [weak self] in
|
|
self?.previousViewController?.view.removeFromSuperview()
|
|
self?.previousViewController?.view.layer?.removeAllAnimations()
|
|
self?.delegate?.navigationController(self!, didShowViewController: viewController, animated: true)
|
|
}
|
|
self.animatePush(contentAnimation)
|
|
CATransaction.commit()
|
|
} else {
|
|
self.delegate?.navigationController(self, didShowViewController: viewController, animated: false)
|
|
}
|
|
}
|
|
|
|
func push(viewController: NSViewController, animation: AnimationBlock?) {
|
|
let navBarAnimation: AnimationBlock? = animation != nil ? navigationBarController?.defaultPushAnimation() : nil
|
|
self.push(viewController: viewController, contentAnimation: animation, navigationBarAnimation: navBarAnimation)
|
|
}
|
|
|
|
func push(viewController: NSViewController, animated: Bool, offsetContentView: Bool = false) {
|
|
if let contentView = self.contentView, let delegate = self.controllerViewDelegate, let leadingConstraint = delegate.leadingConstraint, let applicationView = contentView.superview, offsetContentView {
|
|
applicationView.removeConstraint(leadingConstraint)
|
|
let updatedConstraint = contentView.leadingAnchor.constraint(equalTo: applicationView.leadingAnchor, constant: 60)
|
|
delegate.leadingConstraint = updatedConstraint
|
|
applicationView.addConstraint(updatedConstraint)
|
|
}
|
|
if animated {
|
|
self.push(viewController: viewController, animation: self.defaultPushAnimation())
|
|
} else {
|
|
self.push(viewController: viewController, animation: nil)
|
|
}
|
|
}
|
|
|
|
// MARK: - Popping
|
|
func pop(toViewController viewController: NSViewController, contentAnimation: AnimationBlock?, navigationBarAnimation: AnimationBlock?) {
|
|
guard Set(self.viewControllers).contains(viewController) else { return }
|
|
guard let rootViewController = self.viewControllers.first else { return }
|
|
guard let topViewController = self.topViewController else { return }
|
|
guard topViewController != rootViewController else { return }
|
|
|
|
self.delegate?.navigationController(self, willShowViewController: viewController, animated: (contentAnimation != nil))
|
|
|
|
let viewControllerPosition = self.viewControllers.firstIndex(of: viewController)
|
|
|
|
// Add the new view
|
|
self.contentView?.addSubview(viewController.view, positioned: .below, relativeTo: topViewController.view)
|
|
|
|
// NavigationBar
|
|
if let vc = viewController as? JSNavigationBarViewControllerProvider {
|
|
self.navigationBarController?.pop(toViewController: vc.navigationBarViewController(), animation: navigationBarAnimation)
|
|
}
|
|
|
|
self.controllerViewDelegate?.hasNavigationBar(viewController is JSNavigationBarViewControllerProvider)
|
|
|
|
if let contentAnimation = contentAnimation {
|
|
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.delegate?.navigationController(self, didShowViewController: viewController, animated: true)
|
|
}
|
|
self.animatePop(toView: viewController.view, animation: contentAnimation)
|
|
CATransaction.commit()
|
|
} else {
|
|
topViewController.view.removeFromSuperview()
|
|
let range = (viewControllerPosition! + 1)..<self.viewControllers.count
|
|
self.viewControllers.removeSubrange(range)
|
|
self.delegate?.navigationController(self, didShowViewController: viewController, animated: false)
|
|
}
|
|
}
|
|
|
|
func pop(toViewController viewController: NSViewController, animation: AnimationBlock?) {
|
|
let navBarAnimation: AnimationBlock? = animation != nil ? self.navigationBarController?.defaultPopAnimation() : nil
|
|
self.pop(toViewController: viewController, contentAnimation: animation, navigationBarAnimation: navBarAnimation)
|
|
}
|
|
|
|
func pop(toViewController viewController: NSViewController, animated: Bool) {
|
|
if animated {
|
|
self.pop(toViewController: viewController, animation: defaultPopAnimation())
|
|
} else {
|
|
self.pop(toViewController: viewController, animation: nil)
|
|
}
|
|
}
|
|
|
|
func popViewController(contentAnimation: AnimationBlock?, navigationBarAnimation: AnimationBlock?) {
|
|
guard let previousViewController = self.previousViewController else { return }
|
|
self.pop(toViewController: previousViewController, contentAnimation: contentAnimation, navigationBarAnimation: navigationBarAnimation)
|
|
}
|
|
|
|
func popViewController(animation: AnimationBlock?) {
|
|
let navBarAnimation: AnimationBlock? = animation != nil ? self.navigationBarController?.defaultPopAnimation() : nil
|
|
self.popViewController(contentAnimation: animation, navigationBarAnimation: navBarAnimation)
|
|
}
|
|
|
|
func popViewController(animated: Bool) {
|
|
if animated {
|
|
self.popViewController(animation: self.defaultPopAnimation())
|
|
} else {
|
|
self.popViewController(animation: nil)
|
|
}
|
|
}
|
|
|
|
func popToRootViewController(contentAnimation: AnimationBlock?, navigationBarAnimation: AnimationBlock?) {
|
|
guard let rootViewController = self.viewControllers.first else { return }
|
|
guard let topViewController = self.topViewController else { return }
|
|
guard topViewController != rootViewController else { return }
|
|
|
|
self.pop(toViewController: rootViewController, contentAnimation: contentAnimation, navigationBarAnimation: navigationBarAnimation)
|
|
}
|
|
|
|
func popToRootViewController(animation: AnimationBlock?) {
|
|
let navBarAnimation: AnimationBlock? = animation != nil ? self.navigationBarController?.defaultPopAnimation() : nil
|
|
self.popToRootViewController(contentAnimation: animation, navigationBarAnimation: navBarAnimation)
|
|
}
|
|
|
|
func popToRootViewController(animated: Bool, moveContentViewBack: Bool = false) {
|
|
if let contentView = self.contentView, let delegate = self.controllerViewDelegate, let leadingConstraint = delegate.leadingConstraint, let applicationView = contentView.superview, moveContentViewBack {
|
|
applicationView.removeConstraint(leadingConstraint)
|
|
let updatedConstraint = contentView.leadingAnchor.constraint(equalTo: applicationView.leadingAnchor)
|
|
delegate.leadingConstraint = updatedConstraint
|
|
applicationView.addConstraint(updatedConstraint)
|
|
}
|
|
if animated {
|
|
self.popToRootViewController(animation: self.defaultPopAnimation())
|
|
} else {
|
|
self.popToRootViewController(animation: nil)
|
|
}
|
|
}
|
|
|
|
// MARK: - Animations
|
|
func defaultPushAnimation() -> AnimationBlock {
|
|
return { [weak self] (_, _) in
|
|
let containerViewBounds = self?.contentView?.bounds ?? .zero
|
|
|
|
let slideToLeftTransform = CATransform3DMakeTranslation(-containerViewBounds.width / 2, 0, 0)
|
|
let slideToLeftAnimation = CABasicAnimation(keyPath: #keyPath(CALayer.transform))
|
|
slideToLeftAnimation.fromValue = NSValue(caTransform3D: CATransform3DIdentity)
|
|
slideToLeftAnimation.toValue = NSValue(caTransform3D: slideToLeftTransform)
|
|
slideToLeftAnimation.duration = 0.25
|
|
slideToLeftAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeOut)
|
|
slideToLeftAnimation.fillMode = CAMediaTimingFillMode.forwards
|
|
slideToLeftAnimation.isRemovedOnCompletion = false
|
|
|
|
let slideFromRightTransform = CATransform3DMakeTranslation(containerViewBounds.width, 0, 0)
|
|
let slideFromRightAnimation = CABasicAnimation(keyPath: #keyPath(CALayer.transform))
|
|
slideFromRightAnimation.fromValue = NSValue(caTransform3D: slideFromRightTransform)
|
|
slideFromRightAnimation.toValue = NSValue(caTransform3D: CATransform3DIdentity)
|
|
slideFromRightAnimation.duration = 0.25
|
|
slideFromRightAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeOut)
|
|
slideFromRightAnimation.fillMode = CAMediaTimingFillMode.forwards
|
|
slideFromRightAnimation.isRemovedOnCompletion = false
|
|
|
|
return ([slideToLeftAnimation], [slideFromRightAnimation])
|
|
}
|
|
}
|
|
|
|
func defaultPopAnimation() -> AnimationBlock {
|
|
return { [weak self] (_, _) in
|
|
let containerViewBounds = self?.contentView?.bounds ?? .zero
|
|
|
|
let slideToRightTransform = CATransform3DMakeTranslation(-containerViewBounds.width / 2, 0, 0)
|
|
let slideToRightAnimation = CABasicAnimation(keyPath: #keyPath(CALayer.transform))
|
|
slideToRightAnimation.fromValue = NSValue(caTransform3D: slideToRightTransform)
|
|
slideToRightAnimation.toValue = NSValue(caTransform3D: CATransform3DIdentity)
|
|
slideToRightAnimation.duration = 0.25
|
|
slideToRightAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeOut)
|
|
slideToRightAnimation.fillMode = CAMediaTimingFillMode.forwards
|
|
slideToRightAnimation.isRemovedOnCompletion = false
|
|
|
|
let slideToRightFromCenterTransform = CATransform3DMakeTranslation(containerViewBounds.width, 0, 0)
|
|
let slideToRightFromCenterAnimation = CABasicAnimation(keyPath: #keyPath(CALayer.transform))
|
|
slideToRightFromCenterAnimation.fromValue = NSValue(caTransform3D: CATransform3DIdentity)
|
|
slideToRightFromCenterAnimation.toValue = NSValue(caTransform3D: slideToRightFromCenterTransform)
|
|
slideToRightFromCenterAnimation.duration = 0.25
|
|
slideToRightFromCenterAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeOut)
|
|
slideToRightFromCenterAnimation.fillMode = CAMediaTimingFillMode.forwards
|
|
slideToRightFromCenterAnimation.isRemovedOnCompletion = false
|
|
|
|
return ([slideToRightFromCenterAnimation], [slideToRightAnimation])
|
|
}
|
|
}
|
|
|
|
// MARK: - Storyboard
|
|
override func prepare(for segue: NSStoryboardSegue, sender: Any?) {
|
|
guard segue.identifier == Constants.segueIdentifier else { return }
|
|
guard let destinationController = segue.destinationController as? NSViewController else { return }
|
|
|
|
self.push(viewController: destinationController, animated: false)
|
|
}
|
|
}
|