The Iconmaster

Purveyor of fine digital goods

Transitioning between view controllers in the same window, with Swift – Mac

March 24th, 2015

I suppose because both storyboards for Mac projects and the Swift language are so new, figuring out how to use them together was more aggravating than expected.

This overview is offered as a resource to help others skip some of the pain I encountered, though it should not be taken as a recommendation for production code. I am primarily a designer, and was trying to build a quick-and-dirty prototype. This is not a reference for programmers, but prototypers, and frequently glosses over Swift features that programmers would consider fundamental.

What we want to do is animate nicely between two views in the same window, with the window resizing as necessary to match whichever is the current view.

Launch Xcode. Select File > New > Project. Choose OS X > Application > Cocoa Application. Hit Next. Fill out the product details however you like, but make sure the language is set to Swift (I’m no use to you with Objective-C) and “Use Storyboards” is checked. After the project is created, select “Main.storyboard” in the Project navigator (folder icon in the left-most pane.)

To do this, we’ll need four things on our storyboard:

  • The window controller, already provided. Its size doesn’t matter, but you need to turn off its “Resize” capability in the Attributes Inspector (that’s the fourth button in the inspector on the right side of the Xcode window.) View transitions within user-resizable windows are possible, but beyond the scope of this project.

    Really, turn off window resizing. It’ll be a big headache later if you don’t.

  • A “container” view controller occupying the “window content” relationship to the window controller. Again, just use the initially provided view controlller. Its size doesn’t matter either – we’ll set that in code; but keep in mind anything you place within this view controller’s view will be visible in the app at runtime. (You therefore probably want to leave it devoid of content.)

  • An “origin” view controller that will contain the initial view the user sees. The size you assign its view will determine the size of the window at runtime.

From the Object library in the lower right, find the View Controller and drag it onto the storyboard. Then find the Push Button and place one inside the view controller.

  • A “destination” view controller that will contain the view at which the user arrives after interacting with the origin view. The size you assign its view will determine how the window resizes following that interaction.

Again, drag a view controller onto the storyboard and place a push button within it.

You’ve probably noticed the “origin” and “destination” view controllers display no obvious relationship to the window or “container” controllers. This is because making the transition happen requires both the origin and destination to be designated as children of the container, and this is not possible via storyboard manipulation alone.

We need to build that relationship with Swift code, and that means we need to be able to identify some of these storyboard identities in code. For that, we’ll need the name of the storyboard file – usually “Main.storyboard” – and a couple “Storyboard IDs” we’ll set. Storyboard IDs are set via the Identity inspector, the third button in the inspector on the right side of the Xcode window.

In my example, I’m identifying the “container” view controller as “containerViewController” and the “origin” view controller as “sourceViewController.” It’s not necessary to set a Storyboard ID for the “destination” view controller, which is lucky for us – it means we could set up an indefinite chain of destinations and not have to rewrite any code for the additional view controllers. For clarity’s sake, however, I will refer to the “destination” view controller as if we’d assigned it the Storyboard ID “destinationViewController.”

Assign Storyboard IDs “containerViewController” to the window’s attached view controller and “sourceViewController” to the first additional view controller you created. Make sure you’re selecting the view controller itself and not anything within the controller. Click the blue icon at the top of the controller if you’re unsure. The Storyboard ID field is found in the Identity inspector.

Building the initial parent-child relationship will only involve connecting containerViewController and sourceViewController. We’ll hold off on adding destinationViewController as a child until we really need it.

To do this, we’ll create a new kind of view controller – that is, a “subclass” of Apple’s own NSViewController.

That’s done with File > New > File and selecting OS X > Source > Cocoa Class. Use “ContainerViewController” for the class name and NSViewController for the thing it’s subclassing. Uncheck “Also create XIB file” and leave the language as Swift.

Set the containerViewController to use the ContainerViewController class by selecting it on the storyboard and entering “ContainerViewController” in the Class field on the Identity inspector (third inspector button).

We want the containerViewController to do several things when it first loads, so we’re going to add several expressions within the viewDidLoad() function. Everything we add should be entered following the // Do view setup here comment but before the curly bracket that follows it.

We want to point containerViewController at sourceViewController, which would be easy if they were connected on the storyboard. Since we can’t do it that way, we’re going to direct the ContainerViewController class to the storyboard instead. But the Main.storyboard file isn’t yet something ContainerViewController knows about. We have to “import” it, if you will, as an NSStoryboard object. That looks like this:

let mainStoryboard: NSStoryboard = NSStoryboard(name: "Main", bundle: nil)!

This is not a Swift language guide, but I’ll break this down a bit. let is a Swift keyword that means we’re going to create a new constant – a variable that doesn’t vary. mainStoryBoard is just the name of the constant, which could be anything, but we want the name to make clear what it is. : NSStoryboard signifies this constant will be of the type “NSStoryboard”. The equals sign means that constant will be made equal to what follows, and what follows is a method for getting the storyboard file named “Main.”

The exclamation mark is used to insist on something we know exists but which Xcode isn’t so sure about. As we said, ContainerViewController doesn’t normally know about Main.storyboard; so we’re telling Xcode to take our word for it that there is a storyboard named “Main.”

Programmers refer to this use of the exclamation mark as forced unwrapping, but I prefer to think of it as an “insist.”

Now we have the storyboard as a Swift object and can address anything within it that we’ve assigned a Storyboard ID. Handy!

Next we grab our sourceViewController off the storyboard and render it to another constant:

let sourceViewController = mainStoryboard.instantiateControllerWithIdentifier("sourceViewController") as NSViewController

Since we established mainStoryboard is an NSStoryboard, we can do NSStoryboard things with it like .instantiateControllerWithIdentifier(). That means we tell Swift to grab the view controller with the Storyboard ID in the parentheses – “sourceViewController” – and spin it up so we can put it to use.

(It’s also possible to use .instantiateControllerWithIdentifier() to grab that other kind of controller, a window controller. That may be why we need to specify as NSViewController at the end there.)

At last we can connect containerViewController to sourceViewController as parent and child:

self.insertChildViewController(sourceViewController, atIndex: 0)

Since self here is going to be the containerViewController, we’ve told it to insert sourceViewController as a child of itself. We have to provide the index value because containerViewController could have more than one child controller and we need to explain where in the stack sourceViewController is meant to go. Since it’s the first child controller it goes in at the first index, index 0. (Programmers always start counting at zero, just accept it.)

Interestingly, if you run your app at this point you won’t see sourceViewController get pulled in. That’s because while it’s being added as a child controller “in the background” we haven’t expressly said we want to make it visible yet. (Sigh, I know. Always explaining to computers the obvious…)

Tell containerViewController to show the view in sourceViewController as a subview of its own:


Now it will show up, but it’s likely your window size will be out of whack. We fix that by getting containerViewController to resize itself according to the size of the view in sourceViewController. What we want to mess with is the box around the view, which is called the view’s “frame.” The frame includes both the view box’s point of origin as well as its width and height.

Here’s how we tell containerViewController to match its view’s frame to that of the sourceViewController:

self.view.frame = sourceViewController.view.frame

That’s all we need to do in ContainerViewController. If you run your app now, you should see sourceViewController appear in the app window; but the button you added won’t do anything yet. That’s next.

Return to Main.storyboard. Select the push button you added to sourceViewController. Hold down the control key and drag the blue connecting line into destinationViewController. Release the mouse button, then select “custom” from the Action Segue popup.

What we’ve done is create a new “segue” from sourceViewController to destinationViewController. Custom segues require custom code – that we haven’t written – so this won’t do much yet.

Next, select the push button in destinationViewController and control-drag back toward sourceViewController. Again, select “custom” segue.

A segue is just a transition from one view controller to another. Apple provides the built-in types shown in the Action Segue popup, but none of those will provide the kind of transition we want. That’s why we’re creating our own. Unfortunately this takes quite a bit of code. Hang in there.

We’re going to create another subclass, this time of NSStoryboardSegue, and call it CrossfadeStoryboardSegue.

Select File > New > File and OS X > Source > Cocoa Class. Use “CrossfadeStoryboardSegue” for the class name and NSStoryboardSegue for the thing it’s subclassing. Leave the language as Swift.

The first thing a custom segue has to do is pull in the identities of the controllers it’s connecting. We do that with the following block of code:

override init(identifier: String?,
    source sourceController: AnyObject,
    destination destinationController: AnyObject) {
            var myIdentifier : String
            if identifier == nil {
                myIdentifier = ""
            } else {
                myIdentifier = identifier!
            super.init(identifier: myIdentifier, source: sourceController, destination: destinationController)

Now we’re going to customize what happens when our storyboard segue performs its animation. We do that by adding an

override func perform() {

function underneath the init function we just added. (Make sure both functions are still within the curly brackets which enclose the class definition.) We’re customizing perform() because it’s the block that will execute when the segue is performed.

At this point all three view controllers. (containerViewController, sourceViewController and destinationViewController) are hanging around in code, either initialized within ContainerViewController or connected by the segue. We’re going to create handy references to each. Note that at this point we can count on containerViewController already being a parent to sourceViewController, so we point Xcode to it as such.

let sourceViewController = self.sourceController as! NSViewController
let destinationViewController = self.destinationController as! NSViewController
let containerViewController = sourceViewController.parentViewController!

This last is an important line. My first instinct was to import the storyboard and re-initialize containerViewController by its Storyboard ID. But it seems that would actually create a second instance of the controller. By referring to it here via its parent relationship, we get the existing, already-initialized controller we need.

The exclamation marks are especially confusing here. What we’re saying is something like: for this to work, we really need sourceViewController and destinationViewController to be treated as NSViewControllers (rather than NSWindowControllers, which is the only other possibility). We also really need sourceViewController’s parent controller. So long as we insist on all of that, the rest of our code will work.

So sourceViewController is already a set as a child of destinationViewController. It’s time to do the same for destinationViewController:

containerViewController.insertChildViewController(destinationViewController, atIndex: 1)

We’re inserting at index 1 this time because we don’t want to lose our sourceViewController hanging out at index 0.

Because we’re going to resize the window to match the dimensions of destinationViewController, it’ll make our work easier if we store that size information in some handy variables now:

var targetSize = destinationViewController.view.frame.size
var targetWidth = destinationViewController.view.frame.size.width
var targetHeight = destinationViewController.view.frame.size.height

Remember, the view’s frame is its box containing origin, width and height. So from .view.frame we can grab .view.frame.size, which includes the width and height but not the origin. From .view.frame.size we can grab .view.frame.size.width and .view.frame.size.height, each one still more narrowly focused than .view.frame.size.

(If you wanted to get at the origin alone, you could do .view.frame.size.origin or – more specifically – .view.frame.size.origin.x and .view.frame.size.origin.y. We don’t need those in this case.)

We’re finally ready to animate our view controllers. First we have to prep each controller for animation by granting it a Core Animation layer:

sourceViewController.view.wantsLayer = true
destinationViewController.view.wantsLayer = true

And then we tell containerViewController to make the transition happen:

containerViewController.transitionFromViewController(sourceViewController, toViewController: destinationViewController, 
    options: NSViewControllerTransitionOptions.Crossfade, completionHandler: nil)

All that is just to say: have containerViewController transition from sourceViewController to destinationViewController with the Crossfade option. NSViewControllerTransitionOptions offers a few methods for animating this transition, including sliding the new view controller in from one side or another.

I can only guess at what a completionHandler is, but setting it to “nil” makes Xcode happy.

At this point we have a custom segue that will mostly work, though we have to establish that this is the segue we want our view controllers to use. Go back to Main.storyboard, click on each of the segue arrows connecting sourceViewController and destinationViewController, and in the Attributes inspector (fourth button in the inspector on the right side of the Xcode window) enter “CrossfadeStoryboard” for Segue Class.

You can now run your app and try the button in sourceViewController. It will at least animate the transition to destinationViewController, though you may not like how destinationViewController fits in the window. That’s because we haven’t told the window to resize to fit it.

Go back to CrossfadeStoryboardSegue.swift and add the following on the next available line of the perform() function:


Because we set up sourceViewController and destinationViewController with Core Animation layers earlier, we can now use the animator() feature on their views. animator() can do many things, but here we’re just using it to resize the frames for both source and destination controllers. Our end goal for both controllers is to be the size of the destination controller, which we captured earlier in the variable targetSize. sourceViewController needs to resize itself to match the size of destinationViewController as that controller fades in.

But why does destinationViewController need to resize itself to match the very size we grabbed from destinationViewController? I’m not entirely sure, honestly. My best guess is somewhere in the process destinationViewController adapted its size to that of its parent, containerViewController, requiring us to get it straightened out here.

All that remains, really, is to resize the app window. First, we need to get the current size and location of the window by storing its frame:

var currentFrame = containerViewController.view.window?.frame

containerViewController is still the window’s main view controller, so it’s the best controller to ask about the current window size. view.window means something like “the window containing this view.” And what we need is not the window but its location and size – its frame.

Why the question mark? Question marks in Swift indicate a value that is optional. We’re indicating that containerViewController’s view may or may not be in a window at runtime. It’s almost certainly going to be in a window because of the setup we’ve done elsewhere, but Xcode can’t know that just looking at our custom segue code. So we tell Xcode to relax a little.

What we’re going to do with the next several lines of code is manipulate this currentFrame and then pass it back to the window so it resizes the way we want. There’s a catch, though – we can’t readily manipulate currentFrame in its current form. We have to convert it from its current form (NSRect, the usual for a view’s frame) to a CGRect.

var currentRect = NSRectToCGRect(currentFrame!)

This time the “insist” is required because we indicated window may not be available (window?), but currentFrame depends on window. We could have “insisted” on window instead – window! – and Xcode would have allowed currentFrame to stand without the punctuation.

So now we have the window’s frame stored as currentFrame and a CGRect version called currentRect. We want to resize this to match targetFrame (using targetWidth and targetHeight) but we also want to shift it along the x and y axes so the window seems to resize from the center rather than the top left.

Making that happen is a matter of shifting the frame one half of the difference in dimensions between the original currentFrame and the end goal targetFrame. We need to do the math to get those horizontal and vertical shifts first:

var horizontalChange = (targetWidth - containerViewController.view.frame.size.width)/2
var verticalChange = (targetHeight - containerViewController.view.frame.size.height)/2

horizontalChange is the difference between targetWidth (the width of destinationViewController’s view frame) and the width of containerViewController’s view frame, divided by two. verticalChange is the difference between targetHeight (the height of destinationViewController’s view frame) and the height of containerViewController’s view frame, divided by two.

Now we can take horizontalChange, verticalChange, targetWidth and targetHeight and make a new NSRect from them.

var newWindowRect = NSMakeRect(currentRect.origin.x - horizontalChange, 
    currentRect.origin.y - verticalChange, targetWidth, targetHeight)

NSMakeRect is a function for making rects, natch, and requires four components: the x and y for the upper left origin of the rect, and a width and height. We want our x and y to be the same as that of the current window, but adjusted for the difference between it and the destinationViewController frame origin. The width and height are just the targetWidth and targetHeight we’ve already established.

Finally we can resize the window:

containerViewController.view.window?.setFrame(newWindowRect, display: true, animate: true)

We tell containerViewController to set the frame of its window (which continues to maybe not exist, hence the question mark) to the newWindowRect we just created.

It’s fairly clear that setting animate to false will cause the window to snap to the new size instantly. It’s less clear what display is doing there. Setting it to false has no obvious effect. Well, we certainly want to display the new window so true it is.

At this point your view controller transition, complete with window resize, is finished. Since sourceViewController is no longer visible in the window, it might be wise to dump it from the hierarchy:


And Bob’s your uncle. For as much code as the custom segue requires, you only have to write it once — the same segue can be used anytime you want to create the same kind of transition — even if it’s in an entirely new project. You just have to make sure your source controller is connected as a child to a container controller first.

Download the example project (requires Xcode 6.2).