Scale, Rotate, Fade, and Translate NSView Animations in Swift

This post walks through the Swift implementation of four NSView animations to help you create smooth interactions on macOS:

  1. NSView Fade Animations
  2. NSView Translation Animations
  3. Smooth NSView Scale Animations
  4. NSView Rotation Animations Around The View Center

While implementing NSView animations on macOS, you may run into situations where the anchorPoint property on an NSView is not functioning as expected. This post also addresses two important topics:
a. When Setting the NSView anchorPoint Doesn’t Work
b. Setting the NSView anchorPoint Without the NSView Moving or Jumping

NSView Fade Animations

A fade animation will make the view disappear by smoothly animating from an alpha value of 1 to an alpha value of 0. One option to implement fade animations is to use the view.animator() proxy to configure the animation. To fade out a view, set view.animator().alphaValue to 0.

An easy way to manage a fade animation and set an animation duration is to use NSAnimationContext.runAnimationGroup. NSAnimationContext provides a block-based API for executing animations with support for a completion handler.

// NSView fade-out animation
NSAnimationContext.runAnimationGroup({ context in
    // 1 second animation
    context.duration = 1

    // The view will animate to alphaValue 0
    view.animator().alphaValue = 0
}) {
    // Handle completion
}

NSView Translation Animations

A translation animation will make the NSView move smoothly to a new location. Using the view.animator() proxy, you can set the frame.origin of the view to specify the location to animate to.

An easy way to manage a translation animation and set an animation duration is to use NSAnimationContext.runAnimationGroup. NSAnimationContext provides a block-based API for executing animations with support for a completion handler.

// NSView move animation
NSAnimationContext.runAnimationGroup({ context in
    // 2 second animation
    context.duration = 2
            
    // Animate the NSView downward 20 points
    var origin = view.frame.origin
    origin.y -= 20

    // The view will animate to the new origin
    view.animator().frame.origin = origin
}) {
    // Handle completion
}

Smooth NSView Scale Animations

Zoom animations on an NSView are more complicated than fade and translation animations. One challenge is smoothly animating the subviews to zoom with the parent view. Another challenge is keeping the zooming view centered during the zoom animation.

One solution is to combine multiple animations to produce the desired effect:

  1. Set scaleUnitSquare to double the size
  2. Create a CABasicAnimation and use CATransform3DMakeScale to animate the zoom
  3. Use NSAnimationContext.runAnimationGroup to translate the zooming view, keeping the zooming view centered during the animation
// Set the scale of the view to 2
let doubleSize = NSSize(width: 2, height: 2)
view.scaleUnitSquare(to: doubleSize)
        
// Set the frame to the scaled frame
view.frame = CGRect(
    x: view.frame.origin.x, 
    y: view.frame.origin.y, 
    width: 2 * view.frame.width, 
    height: 2 * view.frame.height
)

// Create the scale animation
let animation = CABasicAnimation()
let duration = 1

animation.duration = duration
animation.fromValue = CATransform3DMakeScale(1.0, 1.0, 1.0)
animation.toValue = CATransform3DMakeScale(2.0, 2.0, 1.0)

// Trigger the scale animation
view.layer?.add(animation, forKey: "transform")
        
// Add a simultaneous translation animation to keep the 
// view center static during the zoom animation
NSAnimationContext.runAnimationGroup({ context in
    // Match the configuration of the scale animation
    context.duration = duration
    context.timingFunction = CAMediaTimingFunction(
        name: CAMediaTimingFunctionName.linear)
            
    // Translate the frame
    origin.x -= view.frame.origin.width / 2
    origin.y -= view.frame.origin.height / 2

    // Trigger the animation
    view.animator().frame.origin = origin
})

When Setting the NSView anchorPoint Doesn’t Work

One caveat of the view.anchorPoint property relates to when anchorPoint is set. If setting the anchorPoint is not working, see if the anchorPoint is being during viewDidLoad(), viewWillAppear(_:), or viewDidAppear(_:). If the anchorPoint is being set during those view functions, try setting the anchorPoint after those functions are called or right before the animation occurs.

Setting the NSView anchorPoint Without the NSView Moving or Jumping

Changing the anchorPoint of an NSView may cause the NSView to jump or move to an unexpected position on screen. The CALayer position, if the NSView is layer backed, is relative to the anchorPoint. Therefore, changing the anchorPoint requires a change in the layer position to keep the view in the same place.

To prevent an NSView from moving or jumping when modifying the anchorPoint, update the position of the layer to point inside the view’s bounds where the anchorPoint is set.

// Prevent the anchor point from modifying views location on screen. This
// example is for moving the anchorPoint to the view’s center
let newAnchorPoint = CGPoint(x: 0.5, y: 0.5)
view?.layer.anchorPoint = newAnchorPoint

let position = view.layer!.position
position.x += view.bounds.maxX * newAnchorPoint.x
position.y += view.bounds.maxY * newAnchorPoint.y
view.layer?.position = position

NSView Rotation Animations Around The View Center

To rotate an NSView about the center, first update the anchorPoint to specify the view’s center by setting the anchorPoint to CGPoint(x: 0.5, y: 0.5). Next to prevent the view from moving or jumping when the anchorPoint is set, update the view’s layer.position property.

Finally, use a CABasicAnimation animating the key path transform.rotation.z to trigger a smooth NSView rotate animation.

// Prepare the anchor point to rotate around the center
let newAnchorPoint = CGPoint(x: 0.5, y: 0.5)
view.layer?.anchorPoint = newAnchorPoint
        
// Prevent the anchor point from modifying views location on screen
let position = view.layer!.position
position.x += view.bounds.maxX * newAnchorPoint.x
position.y += view.bounds.maxY * newAnchorPoint.y
view.layer?.position = position

// Configure the animation
let rotateAnimation = CABasicAnimation(keyPath: "transform.rotation.z")
rotateAnimation.byValue = 2 * CGFloat.pi
rotateAnimation.duration = 2;

// Trigger the animation
CATransaction.begin()
view.layer?.add(rotateAnimation, forKey: "rotate")
CATransaction.setCompletionBlock {
    // Handle animation completion
}
CATransaction.commit()

Animating NSView on macOS in Swift

That’s it! Following the examples in this post allows you to smoothly animate NSView scaling, rotation, fading, and translation on macOS in Swift.