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