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.
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.
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
It turns out that
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.
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.