Swift materialized four years ago at WWDC in June 2014. At the outset, the language was intriguing, but the tooling was…let’s say minimal. Xcode has steadily improved since then — we can even refactor now! But one big chunk of the toolset hasn’t been Swiftified. Interface Builder (IB) stands stoic and steadfast in the face of change. Before we get into the Swift issues, let’s take a look at the history of IB.
Back in the 80’s, some Very Smart People built the Objective-C language and its runtime which was the foundation of the NeXTSTEP operating system. Developers built user interfaces for NeXT computers using Interface Builder. (Yes, IB is 30 years old.) IB creates interface files called NIBs — NeXT Interface Builder. NeXT was acquired by Apple and the NeXTSTEP OS became the basis for Mac OS X, now macOS. macOS begat iOS and IB and its NIB files hung around, quietly doing their jobs. Today, we create user interfaces in Storyboard files, but under the hood, they’re just a bundle of NIBs and a bit of glue.
So NIBs have been around for a while. They clearly get the job done, but how do they work? When you create an app’s GUI using IB, it snapshots the view hierarchy and writes it to a NIB. This is often referred to as a “freeze-dried” version of your UI. When the NIB loads at run time, the view hierarchy is rebuilt using the serialized info in the NIB along with UIView/NSView’s init(coder:) method. This init(coder:) uses an NSCoder instance to rebuild the view with all the settings from IB. And now comes the important part. Once the view is fully instantiated, the IBOutlets are hooked up via the Objective-C runtime, using setValue:forKey:. Because this happens after the view is created (ie, after init returns), the IBOutlets must be Swift optionals, since they are still nil after init completes. Also, because we’re using Key-Value Coding (KVC), IBOutlets are stringly (1) typed. If an outlet name changes, but the NIB isn’t updated, it’ll crash at runtime.
When coding in Swift, we prefer immutability. We’d rather have a let than a var. Unfortunately, IB is built on the idea of on-the-fly mutation at runtime. The Objective-C runtime gives it the power to create objects and then monkey with them, trusting that the attribute names are valid and if they’re not? Crash. Game over. Thanks for playing. Please come again. So we cheat. Devs learn to mark all their IBOutlets with !, to forcibly unwrap the optional. This is an implicit promise to the compiler. “I know this is really an optional, but I swear on my CPU that it will never be nil when I use it.” Everyone has forgotten to hook up an outlet sometime. And it crashes. Force unwrapping an IBOutlet is a hack. It’s a shortcut to make it easier to take IB, a tool that relies on a weakly typed, dynamic runtime, and connect it with Swift, a strongly typed language.
How we fix this? IB has been around for 30 years, relying on Objective-C’s loosey goosey ideas about types and KVC. How can Apple engineers Swiftify it? One way is to pass IBOutlet information to the init(coder:) method. Devs would have to implement hooking up outlets explicitly based on deserializing information in an NSCoder instance. That’s not always straightforward. NSCoder isn’t rocket science, but it’s a bit fiddly and verbose. One of the goals of Swift is give new devs less opportunity to shoot themselves in the foot. Writing this kind of intricate code just to hook up a textfield or a button isn’t very Swifty.
Let’s take a step back and consider similar problems. JSON decoding has highly similar requirements. In parsing JSON, you want to take a serialized format, dependent on string identifiers and map it into the attributes of a newly created Swift object. Early on, this was a common, tedious hurdle that developers had to overcome on their own. With Swift 4, the Codable protocol arrived, and made JSON processing so much easier, albeit through compiler magic (2). But Codable is a generic thing, not just for JSON. Out of the box, Swift comes with a plist encoder/decoder in addition to JSON. You can easily serialize any Codable object to a property list just as easily as JSON. And you can write your own encoder/decoder as well. This repo has examples of a dictionary encoder/decoder and several others.
What if you wrote a NIBDecoder for Codable and declared your view controller as a Codable object? The compiler generates an init(from:) method that takes the serialized data in the NIB file and instantiates your view object, including connecting the IBOutlets. Now it’s all Swifty! No more forced unwrapping of optionals because the outlets are all hooked up during init(from:). NIBs load now by using init(coder:), so this is a direct replacement, but instead of having a developer implement the complicated bits of deserialization, the compiler generates all the code. This would be simple for new developers, but provide the extensibility needed for complex situations to do it yourself.
The Codable approach also adds Swift style error handling. In the case where names don’t match up for some reason, the NIBDecoder can throw and the application has a chance to handle the error in a controlled way. Recoverable errors are a big upgrade from the existing situation, where any error causes an immediate and unforgiving crash. With non-fatal NIB errors, we can also think about handling these UI bundles in more dynamic ways.
I’m handwaving over some complicated processes and we haven’t even talked about backward compatibility or mixing old school NIB loading and this new Codable process. Maybe Codable isn’t the right process for this, but I think it’s tremendously similar. Maybe NIBs would get a new internal format (3) to support Codable deserialization. In any event, Swift is clearly the path forward and its time that Interface Builder evolve to work smoothly with Swift. It’s time to say goodbye to forced unwrapped IBOutlets and let Interface Builder embrace the goodness of immutable outlets. WWDC 2018 is days away, so fingers crossed.
1 – Swift is trying to avoid “stringly” typed items, even with KVC. Swift 3.0 brought use #keyPath() which avoids the need to hard code strings in KVC code. Instead of using a string corresponding to the attribute name, like “volume”, you can give the dotted address of the attribute within the class, like #keyPath(AudioController.volume). With the hardcoded string, there are no checks until runtime. If it’s wrong, it crashes. With the #keypath() version, the compiler finds this property at compile time and substitutes its name. If a name changes, compilation will fail and you can fix the issue. No more runtime surprises!
2 – The inner workings of Codable can get complex. It’s pretty simple for basic usage, but implementing your own encoder is a bit more intricate. Mike Ash has written a nice run down of the details. The magic part happens when the compiler generates encode(to:) and init(from:) methods for your classes that are Codable. The compiler knows all about the types of your attributes, so it can generate this correctly. You could hand write the code as well, but Swift introspection features aren’t mature enough yet that you could implement a Codable-like thing at runtime on your own. All the bits you need just aren’t there. But the compiler does a great job of implementing Codable at compile time, which’ll suffice for now.
3 – NIBs are all XML these days (XIB files). These are super complex files that are impossible to read, even though their text format invites you to try. If you’ve ever had two or more developers working on a project, it’s inevitable that the XIBs in your git repo will cause a “merge hell” situation. This would be a great time for Apple to implement a new kind of UI description file format. Maybe a well documented, clear, concise JSON format. Imagine how great it would be to be able to read the UI file. Or better yet, be able to programmatically generate one, should the need arise.