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 IBOutlet
s? 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 IBOutlet
s will remove the force-unwrapping and we all will live a happy life.