Articles, podcasts and news about Swift development, by John Sundell.

Automatic handling of property wrapper default values

Published on 04 Mar 2021
Basics article available: Properties

Swift’s property wrappers feature enables us to encapsulate a given property value in order to run custom logic whenever that value was created or changed. For example, the following Capitalized type (which was borrowed from my full-length article on property wrappers) lets us automatically capitalize all String values that were assigned to any of our wrapped properties:

@propertyWrapper struct Capitalized {
    var wrappedValue: String {
        didSet { wrappedValue = wrappedValue.capitalized }
    }

    init(wrappedValue: String) {
        self.wrappedValue = wrappedValue.capitalized
    }
}

One really neat aspect of property wrappers is that the compiler will automatically map any default value that we’ve defined at the call site to the above init(wrappedValue:) initializer — meaning that we can keep defining default values just as we normally would, even when a property is wrapped by one of our custom types:

struct Document {
    @Capitalized var name = "Untitled document"
}

That automatic default value mapping even extends to property wrappers that accept additional initializer parameters as well. For example, here we’ve defined a property wrapper that lets us override a given value using a command line argument, and simply by naming our default value parameter wrappedValue (and placing it first within our initializer argument list), we’ll be able to keep assigning default values to the properties that are being wrapped:

@propertyWrapper struct CommandLineOverridable {
    let wrappedValue: Bool
    var flagName: String

    private let defaults = UserDefaults.standard

    init(wrappedValue defaultValue: Bool, flagName: String) {
        self.flagName = flagName

        #if DEBUG
        // First, we check if a command line argument matching
        // our flagName even exists, before retrieving a Bool
        // value for it, since otherwise we'd get 'false' back
        // for flags that weren't passed at all:
        if defaults.object(forKey: flagName) != nil {
            wrappedValue = defaults.bool(forKey: flagName)
            return
        }
        #endif

        wrappedValue = defaultValue
    }
}

Note how we’re using Foundation’s UserDefaults API to parse command line arguments above. To learn more about that, check out “Launch arguments in Swift”.

With the above in place, we now just have to tell each CommandLineOverridable instance what flag that we want to use for each given property, and the compiler will automatically map that property’s default value to our wrappedValue initializer argument:

struct AppConfiguration {
    @CommandLineOverridable(flagName: "sync")
    static var enableSync = true
    @CommandLineOverridable(flagName: "rememberLogin")
    static var rememberLogin = false
}

To learn more about property wrappers in general, check out my full-length article “Property wrappers in Swift”, which goes into a lot more detail.

Thanks for reading!