swiftxcodeappkitnswindow

How to Create a Slide-in Animation on macOS Using AppKit


I'm attempting to create a slide-in animation for a window on macOS using AppKit, but I'm encountering some issues with the animation behaviors.

  1. Animating by Changing the X Coordinate: When I animate the window's x-coordinate from "-width" to "0", the window will initially appears on the left screen and then moves to the right.

    Animating by Changing the X Coordinate

  2. Animating by Changing the Width: If I try to animate by changing the window's width, the contentView gets cropped during the animation.

    Animating by Changing the Width

Here is the example code I'm using to implement the animation:

let contentView = ContentView(viewModel: viewModel)
let dockWindow = NSWindow(
    contentRect: NSRect(x: 0, y: 0, width: 250, height: 900),
    styleMask: [.nonactivatingPanel],
    backing: .buffered,
    defer: false
)
if let focusedScreen = NSScreen.main {
                let screenHeight = focusedScreen.visibleFrame.height
                let targetY = (screenHeight - dockWindow.frame.height) / 2 + focusedScreen.visibleFrame.origin.y
                let targetRect = NSRect(
                    x: focusedScreen.visibleFrame.minX, 
                    y: targetY, 
                    width: dockWindow.frame.width,
                    height: dockWindow.frame.height
                )
                
                let initialRect = NSRect(x: focusedScreen.visibleFrame.minX,
                                         y: targetY,
                                         width: 0, //animate by change the width of the window
                                         height: dockWindow.frame.height)
                dockWindow.setFrame(initialRect, display: true)
                
                NSAnimationContext.runAnimationGroup { context in
                    context.duration = 0.2
                    context.allowsImplicitAnimation = true
                    dockWindow.setFrame(targetRect, display: true)
                } completionHandler: {
                    self.isAnimating = false
                }
            } 
else {
                isAnimating = false
            }

I'm looking for suggestions on how to create a smooth slide-in effect without the issues mentioned. Any insights or alternative approaches would be greatly appreciated.

Thank you in advance for your help!


Solution

  • Instead of animating the slide in of the entire window, I would recommend using a clear borderless transparent window and animating in a subview within the content view using layout constraints.

    Example Storyboard layout:

    Storyboard Layout

    Constraints:

    Constraints

    So initially the Slide In View's leading constraint is negative its width (-250). This will push it out of the visible window and out of view completely. Because any view content outside of their window won't be visible, this will avoid the issue you were running into with it displaying on the second screen left of the one you were trying to slide into.

    To make the Window transparent/borderless use these settings:

    Once we're ready to slide in animate, change the leading constraint's constant to 0 (hugging the left hand side of the window) and then animate laying out the view to slide it in.

    Example code for the SlideInViewController:

    import Cocoa
    
    class SlideInViewController: NSViewController {
        
        @IBOutlet public var leadingConstraint: NSLayoutConstraint?
        @IBOutlet public var slideInView: NSView?
    
        override func viewDidLoad() {
            super.viewDidLoad()
            self.slideInView?.wantsLayer = true
            self.slideInView?.layer?.backgroundColor = .white
        }
        
        public func slideIn() {
            if let screenFrame = NSScreen.main?.visibleFrame {
                let centeredY = (screenFrame.size.height - self.view.frame.size.height) / 2 + screenFrame.origin.y
                let windowFrame = NSRect(
                    x: screenFrame.minX,
                    y: centeredY,
                    width: self.view.frame.size.width,
                    height: self.view.frame.size.height
                )
                self.view.window?.isOpaque = false
                self.view.window?.backgroundColor = .clear
                self.view.window?.setFrame(windowFrame, display: true)
                self.view.window?.orderFront(nil)
                self.leadingConstraint?.constant = 0.0
                NSAnimationContext.runAnimationGroup { context in
                    context.allowsImplicitAnimation = true
                    context.duration = 2.0
                    self.view.layoutSubtreeIfNeeded()
                } completionHandler: {
                    DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + .seconds(2)) {
                        self.slideOut()
                    }
                }
            }
        }
        
        public func slideOut() {
            if let slideInViewWidth = self.slideInView?.frame.size.width {
                self.leadingConstraint?.constant = -slideInViewWidth
                NSAnimationContext.runAnimationGroup { context in
                    context.allowsImplicitAnimation = true
                    context.duration = 2.0
                    self.view.layoutSubtreeIfNeeded()
                } completionHandler: {
                    self.view.window?.orderOut(nil)
                }
                
            }
        }
        
    }
    

    Example call site in AppDelegate applicationDidFinishLaunching:

        func applicationDidFinishLaunching(_ aNotification: Notification) {
            let storyboard = NSStoryboard(name: "Main", bundle: nil)
            let slideInViewController = (storyboard.instantiateController(withIdentifier: "slideInWindow") as? NSWindowController)?.contentViewController as? SlideInViewController
            slideInViewController?.slideIn()
        }
    

    Example of it in action:

    Gif of slide animation