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.
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 Width: If I try to animate by changing the window's width, the contentView gets cropped during the animation.
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!
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:
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:
.clear
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: