Weekly Swift articles, podcasts and tips by John Sundell.

Making properties overridable only in debug builds

Published on 20 Mar 2020

Occasionally, we might want to override certain properties in order to facilitate testing, to be able to work on a new feature, or to debug a problem. That’s particularly common when using Storyboard-based view controllers, and other objects that the system initializes for us, as that typically prevents us from using initializer-based dependency injection.

In those situations, opening up properties to be mutated can be a pragmatic way to give us the flexibility that we need. For example, here we’re enabling a view controller’s UserDefaults and Cache instances to be overridden by making them mutable properties:

class ProfileViewController: UIViewController {
    var userDefaults = UserDefaults.standard
    var cache = Cache()
    ...
}

However, while the above approach opens up doors in terms of flexibility, making a type contain additional mutable state can also end up causing new problems — as we might now accidentally mutate our object in ways we didn’t expect within our production code.

Here’s a way to mitigate that problem, using Swift’s new property wrappers feature in combination with the DEBUG compiler flag. By creating a DebugOverridable property wrapper, we can enforce that the properties that we wish to override during testing and development are not actually overridden within any of our code that we’re shipping to production:

@propertyWrapper
struct DebugOverridable<Value> {
    #if DEBUG
    var wrappedValue: Value
    #else
    let wrappedValue: Value
    #endif
}

Note that we could’ve also used a custom compiler flag above, for example if we also needed to run our tests in release mode.

With the above property wrapper in place, we can now go back to the properties that we only wish to be mutable within DEBUG, and simply mark them with @DebugOverridable to make that happen — no matter which form of abstraction that we use for our dependencies:

class ProfileViewController: UIViewController {
    @DebugOverridable
    var userDefaults = UserDefaults.standard
    @DebugOverridable
    var fileManager: FileManagerProtocol = FileManager.default
    @DebugOverridable
    var cache = Cache()
    @DebugOverridable
    var dateProvider: () -> Date = Date.init
    ...
}

Of course, in an ideal world, we would always be able to do initializer-based dependency injection, and then keep our objects as immutable as possible — but when working with various frameworks and SDKs (including UIKit and AppKit), that’s not always possible — even if there is now a dependency injection-friendly API when using Storyboards, introduced in iOS 13.