
Stop force unwrapping IBOutlets with @Delayed
Oct 23, 2019 16:12 · 3 minute read
Earlier this week I needed a way of initializing a variable just after the “proper initialization” to have access to the other, previously initialized, properties. To do this in Swift, we usually use force-unwrapped types:
final class ThemeManager {
let theme: Theme
let colorScheme: ColorScheme
private(set) var colors: ThemeColors!
init() {
theme = ...
colorScheme = ...
colors = theme.themeColors(for: colorScheme)
}
}
Now the penalty of doing that is that we always have to deal with force-unwrapped optional afterwards. And sometimes it’s just really painful when you want to create a clean API for transforming types and now you have to deal with an Optional, which in fact is not an Optional at all.
Fortunately, Swift 5.1 introduced property wrappers that could actually remove the need of this force-unwrapped type!
Given this @Delayed property wrapper:
@propertyWrapper
struct Delayed<Value> {
private var _value: Value? = nil
var wrappedValue: Value {
get {
guard let value = _value else {
fatalError("Property accessed before being initialized.")
}
return value
}
set {
_value = newValue
}
}
}
We can actually remove the unnecessary ! in the type declaration:
final class ThemeManager {
let theme: Theme
let colorScheme: ColorScheme
@Delayed private(set) var colors: ThemeColors
init() {
theme = ...
colorScheme = ...
colors = theme.themeColors(for: colorScheme)
}
}
This made me supper happy. But, of course, I didn’t stop there.
IBOutlets
I started wondering - can we actually use this technique in IBOutlets? It should be possible, right? Let’s replace:
final class PostsViewController: UIViewController {
@IBOutlet private var contentView: UIView!
}
with:
final class PostsViewController: UIViewController {
@Delayed private var contentView: UIView
}
and run the app. Unfortunately, as expected, it won’t work. Why? It will crash on runtime:
Terminating app due to uncaught exception ‘NSUnknownKeyException’, reason: ‘[setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key contentView.’
Ah okay so it actually needs to be IBOutlet anyways (I thought to myself). Somehow it needs to establish a connection between the interface and the code, probably by using Objc runtime. So I tried:
final class PostsViewController: UIViewController {
@IBOutlet @Delayed private var contentView: UIView
}
but then I got the compile time error:
@IBOutlet property has non-optional type ‘UIView’
So still no luck. Seems like IBOutlet has a constraint on the type (which sounds reasonable). But, fortunately, there is @objc which also exposes the property to Objc runtime and doesn’t have that constraint.
final class PostsViewController: UIViewController {
@Delayed @objc private var contentView: UIView
}
Bingo. When compiled, it worked without any runtime errors. For me, this is a no-brainer and I cannot wait to use this property wrapper everywhere. Hope you find it useful as well.
Now, I think, this is just a matter of time when IBOutlets will remove the force-unwrapping and we all will live a happy life.