Animating Cocoa Window Size When Using Auto Layout

6th April, 2015 — Aral Balkan

Screenshot of the example project.
The button cries out to be pushed.

Window animations with Auto Layout

The one thing you hear over and over again about Apple’s Auto Layout in Cocoa is that you should never, ever directly manipulate the frame of your views when using it. This makes perfect sense since Auto Layout manages the frames of your views for you based on the constraints you use. What you don’t get told nearly as often is the caveat: unless you want to change the size of your window.

Until I realised this, I was hitting my head on the wall trying to alter the Window size by setting my constraints so that their priority was higher than that of my window and then animating the constant to, in effect, push the window out to expand it or pull it in to contract it. Needless to say, this was giving me some odd results (like the Window size jumping to the final dimensions directly and the view itself animating within the set duration to fill it up eventually.)

A little cartography

When working in a complicated project, I frequently jump out of it and create little spikes to test out a feature or a troubleshoot an issue I’m having in isolation. While troubleshooting this particular issue, I also took the opportunity to try out the Cartography autolayout library by Robert Böhnke.

It’s an absolute joy to work with.

Walking through the example

If you want to jump right in and play with the example, you can download the source here. (CartographySpike.zip, 118KB)

Update: And here’s one that shows you how to do the same thing in a project that uses an NSSplitViewController. (CartographySplitViewSpike.zip, 163KB)

Let’s go through the code for the example to see how it works.

First, let’s make sure that we have outlets created for the various boxes we’re going to lay out. I find image views with one-pixel colors set to scale axis independently a quick and easy way to prototype layouts. That’s what I’ve used here.

(While we’re at it, we’ll also create a couple of properties to hold the constraint groups we’ll be creating using Cartography. Although we don’t make use of them in this example, you can animate constraint changes by replacing the constraints in constraint groups and calling layoutSubtreeIfNeeded() on your view from within an animation block.)

In ViewController.swift:

Swift: import Cocoa

class ViewController: NSViewController
{
    @IBOutlet weak var background: NSImageView!
    @IBOutlet weak var longBox: NSImageView!
    @IBOutlet weak var shortBox: NSImageView!
    
    @IBOutlet weak var animateButton: NSButton!
    
    var backgroundConstraintGroup:ConstraintGroup?
    var contentsConstraintGroup:ConstraintGroup?

    // …

Next, let’s create the layout constraints.

When working with Auto Layout, I have a personal preference that might seem rather odd at first: I want to use Interface Builder (IB) to lay out my views and set my constraints but I also want to specify my constraints in code. This allows me to take advantage of the quick, visual prototyping offered by IB (as well as its excellent preview tools for localisation, etc.) and it means that I also know exactly which constraints are being added and have them all in one place in code for easy maintenance. If this sounds like a bit of repitition, it is, but I find that it is worth it to have the best of both worlds.

So, to start off, I lay out my view in IB. For this quick spike, I didn’t actually set any constraints in IB but normally I would have so I could visually fine-tune its responsiveness. Regardless, IB will add some constraints automatically, and I don’t want these, so we start by removing them in viewWillAppear:

Swift: override func viewWillAppear()
{
    super.viewWillAppear()

    // Remove constraints added by 
    // Interface Builder while prototyping.
    self.view.removeConstraints(self.view.constraints)

    // …

Next, let’s set up the constraints using Cartography.

Did I mention that Cartography is a beauty?

I also looked at PureLayout (which led me to try Masonry and KeepLayout as well), but Cartography is the one I found most intuitive.

Continuing with viewWillAppear():

Swift: backgroundConstraintGroup = layout(background)
{
    /* with */ background in
    
    // Make the background hug the edges of its superview exactly.
    background.top == background.superview!.top
    background.bottom == background.superview!.bottom
    background.left == background.superview!.left
    background.right == background.superview!.right
}

contentsConstraintGroup = layout(animateButton, longBox, shortBox)
{
    /* with */ animateButton, longBox, shortBox in

    animateButton.center == animateButton.superview!.center
    
    longBox.width >= 274.0 ~ 1000   // Specify a minimum width for 
                                    // the long box and allow it to
                                    // expand (all constraints at
                                    // required — 1,000 — priority).

    longBox.height == 34.0 ~ 1000   // Static height.

    // Inset the long box by 20 points from the left and 
    // bottom edges of its superview.
    longBox.left == longBox.superview!.left + 20.0 ~ 1000               
    longBox.bottom == longBox.superview!.bottom - 20.0 ~ 1000           
    
    // The short box has a static width and height and is aligned
    // to the right of the long box by the default Cocoa distance.
    // Its inset from the bottom and right of its superview by 
    // 20 points.
    shortBox.width == 78.0 ~ 1000
    shortBox.height == 34.0 ~ 1000
    shortBox.left == longBox.right + 8.0 ~ 1000
    shortBox.right == shortBox.superview!.right - 20.0 ~ 1000 
    shortBox.bottom == shortBox.superview!.bottom - 20.0 ~ 1000
}

println(self.view.constraints)
}

Finally, let’s create an action for the Animate button which, when pressed, animates the size of the Window to make it expand by 200 points in both dimensions while keeping it centred on the same spot by moving its origin to compensate.

Continuing in ViewController.swift:

Swift: @IBAction func animateButtonPressed(sender: NSButton)
{
    // Animate the window’s frame.
    
    NSAnimationContext.runAnimationGroup(
        {
            /* with */ (context: NSAnimationContext!) -> Void in
            
            context.duration = 0.33
            context.allowsImplicitAnimation = true

            var windowFrame:NSRect = self.view.window!.frame
            windowFrame.size.width += 200.0
            windowFrame.size.height += 200.0
            windowFrame.origin.x -= 100.0
            windowFrame.origin.y -= 100.0
            
            self.view.window!.setFrame(windowFrame, display: true, animate: true)
            
        },
        completionHandler:
        {
            () -> Void in
            
            println("Animation complete. Window is now \(self.view.window!.frame.size.width) × \(self.view.window!.frame.size.height) points.")
        }
    )
}

And that’s basically it.

The key thing to remember is that while you shouldn’t be setting the frame of your views directly when using Auto Layout, this doesn’t apply for window size changes.

When you want to smoothly change the size of your window, it’s perfectly permissible to animate its frame and your views that use Auto Layout will animate along beautifully while satisfying their constraints.

While I won’t go into it here, you can also cause the size of the window to change by expanding/collapsing panels in a split view. Find out more about that technique, as well as the window resize method I use here, in the Best Practices for Cocoa Animation session from WWDC 2013 (transcript).