Files

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)
}
}