Swift: Strange tales of initialization
Or the case of the disappearing values
While trying to create a new view controller in Swift I ran into a curious problem with initialization. The class was a relatively simple subclass of UITableViewController
containing a property var values:[String]
and an initializer required init(values:[String])
that delegated to the super.init(style:UITableViewStyle)
initializer. So far, so good. I did find it odd that Swift required me to specify an init(coder:)
initializer. I also found a crash on missing init(nib:bundle:)
that Swift did not warn me about during compilation. Hmmmm.
During testing I discovered that by the time viewDidLoad
was called, the values
property was nil
, despite the property being declared non-optional and being set in the designated initializer. In the debugger I could clearly see the initializer parameter had a value. I could also clearly see the property getting that value. I suspected all sorts of memory corruption or strange ARC issues, but nothing I did mattered: the property that was oh so confidently holding a value came up empty once initialization was complete. Why this happens is a detour into strange tales of initialization in Swift so hold on to your hats, it's going to be a bumpy ride.
The Basics
Swift appears to treat initialization quite differently than Objective-C, even though under the hood it is actually quite similar. They both use two-phase initialization, but Objective-C simply hides the first phase from you.
Phase 1: setting up all stored properties with appropriate default values.
The class must provide values for all of its required properties before calling the superclass initializer. Objective-C can get away with just providing zero values for value types and nil
values for any references because all references are optional. In Swift, that would make optionals essentially useless because a non-optional could in fact be nil
at any time (Swift's value types are also fancier than C structs and intrinsics so can't be initialized with zero'd memory). Swift has no choice but to enforce this rule by providing you access to Phase 1 of initialization.
It might seem like a strange requirement but it has to do with subclassing. As part of initialization the superclass might very well call overridden methods or properties in the subclass, which themselves are highly likely to access properties declared in their own class. Especially non-optional properties that reasonable developers would expect to have values rather than asserting with a fatal error at runtime.
So in summary, Objective-C slaps some zeros* on it and calls it done. Swift has no choice but to offer you access to this initialization phase. And by offer I mean make you an offer you can't refuse.
* Greg Parker points out below that Phase 1 also "runs the zero-argument in-place constructor on ivars with fancy C++ types"
Phase 2: super super super super super
This phase is the familiar one we all know and love. Once Swift has walked the class chain setting all properties to appropriate default values, even including partially executing your initializers up until the point where they call super
, it then resumes executing initialization code starting from the most-derived superclass as it walks back up the chain.
Criticality
It should become clear now that calling the correct parts of the correct initializers in the correct order is critical for Swift to setup a class, well ... correctly.
If you have a funny suspicious feeling in the back of your mind right now congrats: you're a seasoned Objective-C pro. A lot of classes in Objective-C don't follow the proper rules for initialization and/or have incorrectly marked headers. This includes plenty of Apple's own classes. (I don't envy those tasked with auditing all of Foundation, Cocoa, UIKit, etc.)
I lay my woes at the feet of UIKit
UIViewController
is a liar. It does not indicate that init
is not its designated initializer. You'll notice that nowhere have I indicated I was attempting to call init
but all will become clear in a moment.
UITableViewController
is a liar and a cheat. initWithStyle
does not behave like a proper designated initializer, even though it is marked as one. It merely calls UIViewController
's convenience initializer init
.
It turns out that UIViewController
's init
convenience initializer calls back out to the most-derived subclass' initWithNib:bundle:
. That finally explains why I was getting a crash on missing init(nib:bundle:)
in my Swift subclass. The broken designated vs convenience initializer calls are responsible for completely wiping out the property assigned in my Swift designated initializer. In fact all property values are wiped out whether optional or not.
In the grand scheme of things, this ultimately just led to a more brittle controller design: You just have to memorize what properties are required and immediately set them after constructing an instance.
Remember Kids
Let this be a lesson and reminder to you: Get your Objective-C initialization house in order or be prepared to suffer in the land of Swift.
This blog represents my own personal opinion and is not endorsed by my employer.