diff --git a/Simplenote/SPNavigationController.swift b/Simplenote/SPNavigationController.swift index 10bbe18da..215e65609 100644 --- a/Simplenote/SPNavigationController.swift +++ b/Simplenote/SPNavigationController.swift @@ -14,6 +14,11 @@ class SPNavigationController: NSViewController { var topViewController: NSViewController? { viewStack.last } + + private var heightConstraint: NSLayoutConstraint! + private var totalTopPadding: CGFloat { + Constants.buttonViewTopPadding + Constants.buttonViewHeight + } init(initialViewController: NSViewController) { super.init(nibName: nil, bundle: nil) @@ -24,33 +29,29 @@ class SPNavigationController: NSViewController { required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - + override func loadView() { guard let initialViewController = topViewController else { fatalError() } view = NSView() + heightConstraint = view.heightAnchor.constraint(equalToConstant: .zero) let initialView = initialViewController.view backButton = insertBackButton() view.translatesAutoresizingMaskIntoConstraints = false initialView.translatesAutoresizingMaskIntoConstraints = false - view.addSubview(initialView) - - /// "Hint" we wanna occupy as little as possible. This constraint is meant to be broken, but the layout system will - /// attempt to reduce the Height, when possible - /// - let minimumHeightConstraint = view.heightAnchor.constraint(equalToConstant: .zero) - minimumHeightConstraint.priority = .init(1) + attachView(subview: initialViewController.view, below: nil) + resizeWindow(to: initialViewController.view, animated: false) NSLayoutConstraint.activate([ initialView.leadingAnchor.constraint(equalTo: view.leadingAnchor), initialView.trailingAnchor.constraint(equalTo: view.trailingAnchor), initialView.topAnchor.constraint(equalTo: backButton.bottomAnchor), initialView.bottomAnchor.constraint(equalTo: view.bottomAnchor), - minimumHeightConstraint + heightConstraint ]) } @@ -71,10 +72,10 @@ class SPNavigationController: NSViewController { view.addSubview(backButton) NSLayoutConstraint.activate([ - backButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 10), - backButton.topAnchor.constraint(equalTo: view.topAnchor, constant: 30), - backButton.widthAnchor.constraint(equalToConstant: 50), - backButton.heightAnchor.constraint(equalToConstant: 30) + backButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: Constants.buttonViewLeadingPadding), + backButton.topAnchor.constraint(equalTo: view.topAnchor, constant: Constants.buttonViewTopPadding), + backButton.widthAnchor.constraint(equalToConstant: Constants.buttonViewWidth), + backButton.heightAnchor.constraint(equalToConstant: Constants.buttonViewHeight) ]) return button @@ -91,25 +92,15 @@ class SPNavigationController: NSViewController { let currentView = topViewController?.view attach(child: viewController) - - /// Disable Bottom Constraint - /// This allows for the enclosing NSWindow to resize, just enough to fit the `nextViewController.view` - /// - if let currentView, let bottomConstraint = view.firstContraint(firstView: currentView, firstAttribute: .bottom) { - bottomConstraint.isActive = false - } - - guard let (leadingAnchor, trailingAnchor) = attachView(subview: viewController.view, below: currentView) else { - return - } + attachView(subview: viewController.view, below: currentView) + resizeWindow(to: viewController.view, animated: animated) guard animated else { + currentView?.removeFromSuperview() + backButton.isHidden = hideBackButton return } - leadingAnchor.constant = view.frame.width - trailingAnchor.constant = view.frame.width - animateTransition(slidingView: viewController.view, fadingView: currentView, direction: .trailingToLeading) { currentView?.removeFromSuperview() } @@ -120,45 +111,53 @@ class SPNavigationController: NSViewController { viewStack.append(child) } - @discardableResult - private func attachView(subview: NSView, below siblingView: NSView?) -> (leading: NSLayoutConstraint, trailing: NSLayoutConstraint)? { + private func attachView(subview: NSView, below siblingView: NSView?) { + subview.translatesAutoresizingMaskIntoConstraints = false + if let siblingView { view.addSubview(subview, positioned: .below, relativeTo: siblingView) } else { view.addSubview(subview) } - subview.translatesAutoresizingMaskIntoConstraints = false - - let leadingAnchor = subview.leadingAnchor.constraint(equalTo: view.leadingAnchor) - let trailingAnchor = subview.trailingAnchor.constraint(equalTo: view.trailingAnchor) - NSLayoutConstraint.activate([ - leadingAnchor, - trailingAnchor, - subview.topAnchor.constraint(equalTo: backButton.bottomAnchor), - subview.bottomAnchor.constraint(equalTo: view.bottomAnchor) + subview.leadingAnchor.constraint(equalTo: view.leadingAnchor), + subview.trailingAnchor.constraint(equalTo: view.trailingAnchor), + subview.topAnchor.constraint(equalTo: backButton.bottomAnchor) ]) + } + + private func resizeWindow(to subview: NSView, animated: Bool) { + let finalHeight = subview.fittingSize.height + totalTopPadding + guard animated else { + heightConstraint.constant = finalHeight + return + } - return (leading: leadingAnchor, trailing: trailingAnchor) + NSAnimationContext.runAnimationGroup { context in + context.duration = 0.4 + context.timingFunction = .init(name: .easeInEaseOut) + + heightConstraint.animator().constant = finalHeight + } } // MARK: - Remove view from stack - func popViewController() { + func popViewController(animated: Bool = true) { guard viewStack.count > 1, let currentViewController = viewStack.popLast(), let nextViewController = viewStack.last else { return } - - /// Disable Bottom Constraint - /// This allows for the enclosing NSWindow to resize, just enough to fit the `nextViewController.view` - /// - if let currentBottomConstraint = view.firstContraint(firstView: currentViewController.view, firstAttribute: .bottom) { - currentBottomConstraint.isActive = false - } attachView(subview: nextViewController.view, below: currentViewController.view) - + resizeWindow(to: nextViewController.view, animated: animated) + + guard animated else { + dettach(child: currentViewController) + backButton.isHidden = hideBackButton + return + } + animateTransition(slidingView: currentViewController.view, fadingView: nextViewController.view, direction: .leadingToTrailing) { self.dettach(child: currentViewController) } @@ -182,9 +181,14 @@ class SPNavigationController: NSViewController { return } + if direction == .trailingToLeading { + leadingConstraint.constant = view.frame.width + trailingConstraint.constant = view.frame.width + } + let multiplier: CGFloat = direction == .leadingToTrailing ? 1 : -1 let alpha: CGFloat = direction == .leadingToTrailing ? 1 : 0 - + NSAnimationContext.runAnimationGroup { context in context.duration = 0.4 context.timingFunction = .init(name: .easeInEaseOut) @@ -210,3 +214,10 @@ extension SPNavigationController { backButton.layer?.backgroundColor = .clear } } + +private struct Constants { + static let buttonViewWidth = CGFloat(50) + static let buttonViewHeight = CGFloat(30) + static let buttonViewTopPadding = CGFloat(30) + static let buttonViewLeadingPadding = CGFloat(10) +}