Weekly Swift articles, podcasts and tips by John Sundell.

Property wrappers in Swift

Published on 19 Jan 2020

When dealing with properties that represent some form of state, it’s very common to have some kind of associated logic that gets triggered every time that a value is modified. For example, we might validate each new value according to a set of rules, we might transform our assigned values in some way, or we might be notifying a set of observers whenever a value was changed.

In those kinds of situations, Swift 5.1’s property wrappers feature can be incredibly useful, as it enables us to attach such behaviors and logic directly to our properties themselves — which often opens up new opportunities for code reuse and generalization. This week, let’s take a look at how property wrappers work, and explore a few examples of situations in which they could be used in practice.

Transparently wrapping a value

Like the name implies, a property wrapper is essentially a type that wraps a given value in order to attach additional logic to it — and can be implemented using either a struct or a class by annotating it with the @propertyWrapper attribute. Besides that, the only real requirement is that each property wrapper type should contain a stored property called wrappedValue, which tells Swift which underlying value that’s being wrapped.

For example, let’s say that we wanted to create a property wrapper that automatically capitalizes all String values that were assigned to it. That might be implemented like this:

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

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

Note how we need to explicitly capitalize any string that was passed into our initializer, since property observers are only triggered after a value or object was fully initialized.

To apply our new property wrapper to any of our String properties, we simply have to annotate it with @Capitalized — and Swift will automatically match that annotation to our above type. Here’s how we might do that to ensure that a User type’s firstName and lastName properties are always capitalized:

struct User {
    @Capitalized var firstName: String
    @Capitalized var lastName: String
}

The cool thing about property wrappers is that they act completely transparently, which means that we can still work with our above two properties as if they were normal strings — both when initializing our User type, and when modifying its property values:

// John Appleseed
var user = User(firstName: "john", lastName: "appleseed")

// John Sundell
user.lastName = "sundell"

Similarly, as long as a property wrapper defines an init(wrappedValue:) initializer (like our Capitalized type does) — then we can even natively assign default values to our wrapped properties, like this:

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

So property wrappers enable us to transparently wrap and modify any stored property — using a combination of an @propertyWrapper-marked type, and annotations matching the name of that type. But that’s just the beginning.

A property’s properties

Property wrappers can also have properties of their own, which enables further customization, and even makes it possible to inject dependencies into our wrapper types.

As an example, let’s say that we’re working on a messaging app that uses Foundation’s UserDefaults API to store various user settings and other pieces of lightweight data on disk. Doing so typically involves writing some form of mapping code for synchronizing each value with its underlying UserDefaults storage — which often needs to be replicated for each piece of data that we’re looking to store.

However, by implementing that sort of logic within a generic property wrapper, we could make it easily reusable — as doing so would let us simply attach our wrapper to any property that we’d like to be backed by UserDefaults. Here’s what such a wrapper might look like:

@propertyWrapper struct UserDefaultsBacked<Value> {
    let key: String
    var storage: UserDefaults = .standard

    var wrappedValue: Value? {
        get { storage.value(forKey: key) as? Value }
        set { storage.setValue(newValue, forKey: key) }
    }
}

Just like any other struct, our above UserDefaultsBacked type will automatically get a memberwise initializer with default arguments for all properties that have a default value — which means that we’ll be able to initialize instances of it by simply specifying which UserDefaults key that we want each property to be backed by:

struct SettingsViewModel {
    @UserDefaultsBacked(key: "mark-as-read")
    var autoMarkMessagesAsRead: Bool

    @UserDefaultsBacked(key: "search-page-size")
    var numberOfSearchResultsPerPage: Int
}

The compiler will automatically infer which type to specialize our generic UserDefaultsBacked wrapper with, based on the type of the property that it’s wrapping.

The above setup makes our new property wrapper easy to use whenever we want a property to be backed by UserDefaults.standard, but since we parameterized that dependency, we could also choose to use a custom instance if we’d like — for example to facilitate testing, or to be able to share values between multiple apps within the same app group:

extension UserDefaults {
    static var shared: UserDefaults {
        let combined = UserDefaults.standard
        combined.addSuite(named: "group.johnsundell.app")
        return combined
    }
}

struct SettingsViewModel {
    @UserDefaultsBacked(key: "mark-as-read", defaults: .shared)
    var autoMarkMessagesAsRead: Bool

    @UserDefaultsBacked(key: "search-page-size", defaults: .shared)
    var numberOfSearchResultsPerPage: Int
}

For more information about using UserDefaults to share data between multiple apps, check out “The power of UserDefaults in Swift”.

However, our above implementation has a quite significant flaw. Even though both of our above two properties are declared as non-optional, their actual values will still be optionals, since our UserDefaultsBacked type specifies Value? as its wrappedValue property’s type.

Thankfully, that flaw can be quite easily fixed. All we have to do is to add a defaultValue property to our wrapper, which we’ll then use whenever our underlying UserDefaults storage didn’t contain a value for our property’s key:

@propertyWrapper struct UserDefaultsBacked<Value> {
    let key: String
    let defaultValue: Value
    var storage: UserDefaults = .standard

    var wrappedValue: Value {
        get {
            let value = storage.value(forKey: key) as? Value
            return value ?? defaultValue
        }
        set {
            storage.setValue(newValue, forKey: key)
        }
    }
}

With the above in place, we’re now able to turn both of our properties into non-optionals, like this:

struct SettingsViewModel {
    @UserDefaultsBacked(key: "mark-as-read", defaultValue: true)
    var autoMarkMessagesAsRead: Bool

    @UserDefaultsBacked(key: "search-page-size", defaultValue: 20)
    var numberOfSearchResultsPerPage: Int
}

That’s really nice. However, some of our UserDefaults values are likely to actually be optionals, and it would be unfortunate if we had to constantly specify nil as the default value for those properties — as that’s not something that we have to do when not using property wrappers.

To address that, let’s also add a convenience API to our wrapper for whenever its Value type conforms to ExpressibleByNilLiteral (which Optional does) — in which we’ll automatically insert nil as the default value:

extension UserDefaultsBacked where Value: ExpressibleByNilLiteral {
    init(key: String, storage: UserDefaults = .standard) {
        self.init(key: key, defaultValue: nil, storage: storage)
    }
}

With the above change in place, we can now freely use our UserDefaultsBacked wrapper with both optional and non-optional values with ease:

struct SettingsViewModel {
    @UserDefaultsBacked(key: "mark-as-read", defaultValue: true)
    var autoMarkMessagesAsRead: Bool

    @UserDefaultsBacked(key: "search-page-size", defaultValue: 20)
    var numberOfSearchResultsPerPage: Int

    @UserDefaultsBacked(key: "signature")
    var messageSignature: String?
}

However, there is one more thing that we’ll need to take into account, since we’ll now be able to assign nil to a UserDefaultsBacked property. To avoid crashes in such situations, we’ll have to update our property wrapper to first check if any assigned value is nil before proceeding to store it within the current UserDefaults instance, like this:

// Since our property wrapper's Value type isn't optional, but
// can still contain nil values, we'll have to introduce this
// protocol to enable us to cast any assigned value into a type
// that we can compare against nil:
private protocol AnyOptional {
    var isNil: Bool { get }
}

extension Optional: AnyOptional {
    var isNil: Bool { self == nil }
}

@propertyWrapper struct UserDefaultsBacked<Value> {
    ...

    var wrappedValue: Value {
        get { ... }
        set {
            if let optional = newValue as? AnyOptional, optional.isNil {
                storage.removeObject(forKey: key)
            } else {
                storage.setValue(newValue, forKey: key)
            }
        }
    }
}

The fact that property wrappers are implemented as actual types gives us a lot of power — as we can give them properties, initializers, and even extensions — which in turn enables us to both make our call sites really neat and clean, and to make full use of Swift’s robust type system.

Decoding and overriding

Although most property wrappers are likely going to be implemented as structs in order to utilize value semantics, sometimes we might want to opt for reference semantics by using a class instead.

For example, let’s say that we’re working on a project that uses feature flags to enable testing and gradual rollouts of new features and experiments, and that we want to build a property wrapper that’ll let us specify such flags in different ways. Since we’ll want to share those values across our code base, we’ll implement that wrapper as a class:

@propertyWrapper final class Flag<Value> {
    let name: String
    var wrappedValue: Value

    fileprivate init(name: String, defaultValue: Value) {
        self.name = name
        self.wrappedValue = defaultValue
    }
}

With our new wrapper type in place, we can now start defining our flags as properties within an encapsulating FeatureFlags type — which’ll act as a single source of truth for all of our app’s feature flags:

struct FeatureFlags {
    @Flag(name: "feature-search", defaultValue: false)
    var isSearchEnabled: Bool

    @Flag(name: "experiment-note-limit", defaultValue: 999)
    var maximumNumberOfNotes: Int
}

At this point, the above Flag property wrapper might seem a bit redundant, given that it doesn’t actually do anything other than store its wrappedValue — but that’s about to change.

A very common way to use feature flags is to download their values over the network, for example every time that the app launches, or according to a certain time interval. However, even when using Codable, there’s typically a fair amount of boilerplate involved in making that happen — given that we’ll most likely want to fall back to our app’s default values for flags that may not have been added to our backend yet (or those that have been removed after a test or rollout was completed).

So let’s use our Flag property wrapper to implement that form of decoding. Since we want to use each flag’s name as its coding key, the first thing we’ll do is to define a new CodingKey type that’ll let us do just that:

private struct FlagCodingKey: CodingKey {
    var stringValue: String
    var intValue: Int?

    init(name: String) {
        stringValue = name
    }
    
    // These initializers are required by the CodingKey protocol:

    init?(stringValue: String) {
        self.stringValue = stringValue
    }

    init?(intValue: Int) {
        self.intValue = intValue
        self.stringValue = String(intValue)
    }
}

Next, we’re going to need a way to reference each of our flags without knowing their generic type — but rather than resorting to full type erasure, we’re going to add a protocol called DecodableFlag, which’ll enable each flag to decode its own value according to its Value type:

private protocol DecodableFlag {
    typealias Container = KeyedDecodingContainer<FlagCodingKey>
    func decodeValue(from container: Container) throws
}

Besides enabling our server to fully control our app’s feature flags, it would also be really useful to be able to add local overrides for individual flags as well. That way we could specify exactly which values to use when writing UI tests, and easily enable a new feature while working on it. So let’s also add that capability to our Flag wrapper, which we’ll do by once again using UserDefaults (which has the somewhat hidden feature of being able to parse command line arguments), giving us a decoding implementation that looks like this:

extension Flag: DecodableFlag where Value: Decodable {
    fileprivate func decodeValue(from container: Container) throws {
        // This enables us to pass an override using a command line
        // argument matching the flag's name:
        if let value = UserDefaults.standard.value(forKey: name) {
            if let matchingValue = value as? Value {
                wrappedValue = matchingValue
                return
            }
        }

        let key = FlagCodingKey(name: name)

        // We only want to attempt to decode a value if it's present,
        // to enable our app to fall back to its default value
        // in case the flag is missing from our backend data:
        if let value = try container.decodeIfPresent(Value.self, forKey: key) {
            wrappedValue = value
        }
    }
}

For more information about using command line arguments to implement debugging utilities, check out “Launch arguments in Swift”.

Finally, let’s complete our decoding implementation by making FeatureFlags conform to Decodable. Here we’ll use reflection to dynamically iterate over each of our flag properties, and we’ll then ask each flag to attempt to decode its value using the current decoding container, like this:

extension FeatureFlags: Decodable {
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: FlagCodingKey.self)

        for child in Mirror(reflecting: self).children {
            guard let flag = child.value as? DecodableFlag else {
                continue
            }

            try flag.decodeValue(from: container)
        }
    }
}

While we did have to implement a bit of underlying infrastructure, we now have a very flexible feature flag system in place — with the ability to specify flag values both server- and client side, support for overriding flags via command line arguments, and for new flags to be defined simply by adding a @Flag-annotated property to our FeatureFlags type.

Projected values

As we’ve explored so far in this article, one of the major benefits of property wrappers is that they enable us to add logic and behaviors to properties in a way that doesn’t impact our call sites at all — as values are read and written the exact same way regardless of whether a property is wrapped or not.

However, sometimes we might actually want to access a property wrapper itself, rather than the value that it’s wrapping. That’s especially common when building UIs using Apple’s new SwiftUI framework, which makes heavy use of property wrappers to implement its various data binding APIs.

For example, here we’re building a QuantityView that enables some form of quantity to be specified using a Stepper view. In order to bind that piece of state to our view, we’ve annotated it with @State, and we’re then giving our stepper direct access to that wrapped state (rather than just its current Int value) by passing it prefixed with $ — like this:

struct QuantityView: View {
    ...
    @State private var quantity = 1

    var body: some View {
        // Passing a wrapped property prefixd with "$" passes
        // the property wrapper itself, rather than its value:
        Stepper("Quantity: \(quantity)",
            value: $quantity,
            in: 1...99
        )
    }
}

The above feature might seem like something that was tailor-made for SwiftUI, but it’s actually a capability that can be added to any property wrapper, for example our Flag type from before. That “dollar-prefixed” version of our above property is known as its wrapper’s projected value, and is implemented by adding a projectedValue property to any wrapper type:

@propertyWrapper final class Flag<Value> {
    var projectedValue: Flag { self }
    ...
}

Just like that, any Flag-annotated property can now also be passed as a projected value — that is, as a reference to its wrapper itself. Again, that’s not something that’s coupled with SwiftUI, in fact we could adopt the same sort of pattern when using UIKit as well — for example by having a UIViewController accept an instance of Flag when initialized.

Here’s an example of how we might do just that to implement a view controller that’ll let us toggle a given Bool-based feature flag on or off when using a debug build of our app:

class FlagToggleViewController: UIViewController {
    private let flag: Flag<Bool>
    private lazy var label = UILabel()
    private lazy var toggle = UISwitch()

    init(flag: Flag<Bool>) {
        self.flag = flag
        super.init(nibName: nil, bundle: nil)
    }
    
    ...

    override func viewDidLoad() {
        super.viewDidLoad()

        label.text = flag.name
        toggle.isOn = flag.wrappedValue

        toggle.addTarget(self,
            action: #selector(toggleFlag),
            for: .valueChanged
        )
        
        ...
    }

    @objc private func toggleFlag() {
        flag.wrappedValue = toggle.isOn
    }
}

To initialize the above view controller, we’ll use the same $-prefix-based syntax as when passing an @State reference when using SwiftUI:

let flags: FeatureFlags = ...

let searchToggleVC = FlagToggleViewController(
    flag: flags.$isSearchEnabled
)

We’ll definitely explore the above use of property wrappers more in upcoming articles — as it could enable us to make our code more declarative, to implement property-based observation APIs, to perform quite sophisticated data binding, and much more.

Conclusion

Property wrappers is definitely one of the most exciting new features in Swift 5.1 — as it opens up a lot of doors for code reuse and customizability, and enables powerful new ways to implement property-level functionality. Even outside of declarative frameworks like SwiftUI, there’s a ton of potential use cases for property wrappers, many of which won’t require us to make any big changes to our overall code — as property wrappers mostly operate completely transparently.

However, that transparency can be both an advantage and a liability. On one hand, it enables us to access and assign wrapped properties the exact same way as unwrapped ones — but on the other hand, the risk is that we’ll end up hiding too much functionality behind what might be a quite non-obvious abstraction.

What do you think? Have you started adopting property wrappers, or do you have a use case that you think they’d be a great fit for? Let me know — along with your comments, questions and feedback — either via Twitter or email.

Thanks for reading! 🚀