TL;DR: If you’re excited about SwiftUI but still need to write AppKit, use NSStackViews. They’ll bring some similar benefits, and make it easier to migrate to SwiftUI later.
The nib file revolution is over
For macOS and iOS projects, the future is certainly SwiftUI. The SwiftUI approach to interface layout using code is an efficient, easy-to-understand representation of internal state. But what if you are working with a legacy code base, and aren’t ready to fully commit to SwiftUI?
As a long-time macOS/iOS developer, I’ve been using “nib” files for years. Decades! These data representations of a user interface were revolutionary at the time: Not only could you “WYSIWYG” lay out your interface visually, you could connect your code with the interface elements bidirectionally, avoiding a ton of glue code required in other interface-building paradigms. Along the way, Apple also gave us Storyboard files, with additional advantages such as transitions and navigation.
However, as the years have gone by, many disadvantages to using nib files have cropped up. Adaptive UIs means that pixel-based layouts were are longer relevant in many cases. Unlike the traditional springs-and-struts, layout constraints are tedious to specify and get just right. Merge conflicts in a multi-author team happen more than we’d like. And in a complex project with many nested custom views and view controllers, boilerplate glue code has become more and more necessary.
Pulling SwiftUI-like benefits into AppKit
My Remotion dock
Our native Mac app, Remotion, started out being written in AppKit before SwiftUI came on the scene. Of course, we recognize that SwiftUI is where we all should be moving — as Paul Hudson said, "SwiftUI is the future. Not the distant future, the imminent future.”
Some benefits of SwiftUI over legacy AppKit (and UIKit) building techniques include:
Easier to adapt to varying screen/window sizes
Not having to deal with merge conflicts in nib/storyboard files
A terse, easy-to-understand representation of layout and internal state
Though we will be gradually implementing some pieces of Remotion in SwiftUI, we can still make headway toward that future by building our interfaces in a way that’s closer to SwiftUI than to legacy, nib/storyboard-based interfaces.
Stacks and stacks of stacks
If you’ve looked into SwiftUI, you’ll notice that the HStack and VStack constructs are ubiquitous. There’s a reason for that: many user interfaces can be decomposed into nested groups of horizontal or vertical “stacks.” Thinking of a layout in terms of nested stacks is also a great way to make sure that a layout is adaptive to changing container sizes, such as the variety of iPhone screen sizes or resizable Mac window sizes.
For instance, we could decompose the main window of Apple’s Calendar application into a series of nested vertical and horizontal stacks.
When not to use stack views
Remotion's preferences, implemented with nibs—not stack views
Of course, some kinds of windows and views are still great candidates for specifying using a nib or a storyboard. Static, unchanging layouts such as preference panes or inspector panels, or custom alerts and dialogs are great for nibs; our App uses them in these cases. Complex, multi-level interfaces such as assistants and master-detail navigation can be built up with storyboards. (Our onboarding window is ideal for this technique.)
However much in the Remotion application is a dynamic layout, consisting of a one- or two-dimensional collection of interface elements that can’t really be specified in a graphical manner because the contents aren’t determined at implementation time. For these, we need to use one of Apple’s other container classes. For some, like our call window that shows a matrix of circular videos of participants in a call, the NSCollectionView is the best approach. In other cases, such as our lists of users and rooms, the NSTableView is the best way to present the data.
But for mixed-bag situations, such as our inspectors for users and rooms, we’ve found that the best way to build up the interface is with nested stack views.
We’ve found stack views are best implemented programmatically
While it’s possible to build a nib file with stack views, we’ve avoided doing this for two main reasons:
The contents of the stack view often aren’t known until run-time. For example, different views are presented depending on whether a user depicted is the user of the app or one of their teammates, whether they are online or offline, whether they are currently talking to another teammate, etc.
We have a lot of custom views that require their own custom view controllers, and it’s not possible to embed a view controller with associated view within a nib. We found ourselves creating empty container NSView objects in the nib, and then writing glue code to insert a view controller and view into that container. That ended up being quite a mess!
As an example, here is an inspector for one of our rooms, as exploded by Xcode’s “Debug View Hierarchy” tool. Notice how many stack views are used to compose the layout.
How to programmatically write view controllers with stack views
In order to break out of the pattern of loading a view from a nib file and a view controller, the easiest way is to override loadView() and set the value of the view controller’s view yourself. Be sure not to call super.loadView() since that will attempt to load a nib file.
Note: We’ve found through trial and error that setting an NSStackView or NSCollectionView to be view controller’s view can result in some errors when the view was deallocated, so we always add an empty NSView and add our stack view or collection view nested inside of this.
Because we are creating a lot of stack views in code, we’ve created a new convenience constructor so we can specify typical parameters quickly. It’s based loosely on the built-in init(views: [NSView]) which returns a horizontal stack view with the given views in the “leading” gravity area, and has translatesAutoresizingMaskIntoConstraints set to false. In our constructor, everything has a default value so it can be left unspecified.
This allows us to specify a stack view with very little code! For instance:
Looking ahead to SwiftUI
Although this code looks nothing like SwiftUI and migrating will still be a major investment, we get some similar benefits: Code is broken down into small components. Merge conflicts are rare and easy to resolve. And the UI already uses the helpful conceptual layout of nested horizontal stack and vertical stacks—so we won’t need to rethink the view hierarchy when it’s time to migrate.
Of course, SwiftUI still has a number of advantages. In areas where we are starting to migrate our legacy stackview-based layouts to SwiftUI, the amount of setup and layout code is greatly reduced and it’s much easier to conceptualize how data flows to build up the interface.
How is your team approaching adopting SwiftUI? Would love to hear your thoughts and feedback at dan at remotion dot com.