Skip to content

Commit

Permalink
Merge pull request #1204 from Automattic/charlie/1187-window-animation
Browse files Browse the repository at this point in the history
Magic Links: Fix window resizing animation + some clean up
  • Loading branch information
charliescheer authored Jul 26, 2024
2 parents a31ea9e + cf8a7a5 commit 7d63262
Showing 1 changed file with 60 additions and 49 deletions.
109 changes: 60 additions & 49 deletions Simplenote/SPNavigationController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
])
}

Expand All @@ -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
Expand All @@ -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()
}
Expand All @@ -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)
}
Expand All @@ -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)
Expand All @@ -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)
}

0 comments on commit 7d63262

Please sign in to comment.