IBOutlets in Xcode

Stop force unwrapping IBOutlets with @Delayed

Oct 23, 2019 16:12 · 3 minute read #swift #IBOutlets #iOS #property wrappers

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.



share:

Comments