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

Accessing a Swift property wrapper’s enclosing instance

Published on 19 Jan 2021
Basics article available: Properties

Like its name implies, Swift’s property wrappers feature enables us to wrap a given property value within a custom type, which in turn lets us apply transforms and run other kinds of logic whenever that value is modified.

By default, a property wrapper is completely disconnected from the enclosing types in which it’s being used, which can prove to be quite limiting in certain situations. For example, we can’t perform method calls or otherwise interact with a wrapper’s enclosing instance, since the standard API doesn’t provide us with such a reference.

However, it turns out that there is an alternative, somewhat hidden API that actually does give us access to each enclosing instance, which can enable us to adopt some really interesting patterns. Let’s take a look!

⚠️ Although all of the language features that are covered in this article are official parts of Swift, we’re going to make use of an API that has an underscore prefix, which should always be done with caution, since such APIs are bound to change at any point.

Building Mobile Apps at Scale

Building Mobile Apps at Scale: Based on learnings from scaling the development of the Uber app over four years — this free, 200-page book will help you overcome 39 of the most commonly faced challenges when building large iOS apps. The book is free to download for a limited time, so grab your copy now.

Getting started

By default, a Swift property wrapper is implemented by annotating a given type (usually a struct) with the @propertyWrapper attribute, and by then declaring a wrappedValue property within that type that’ll act as the underlying storage for the value that’s being wrapped. For example like this:

@propertyWrapper
struct MyWrapper<Value> {
    var wrappedValue: Value
}

However, if we take a look at the Swift Evolution proposal for the property wrappers feature, we can see that it also mentions a second, alternative way of handling a wrapper’s value — through a static subscript that looks like this:

@propertyWrapper
struct EnclosingTypeReferencingWrapper<Value> {
    static subscript<T>(
        _enclosingInstance instance: T,
        wrapped wrappedKeyPath: ReferenceWritableKeyPath<T, Value>,
        storage storageKeyPath: ReferenceWritableKeyPath<T, Self>
    ) -> Value {
        ...
    }
    
    ...
}

Apart from the current enclosing instance, the above API lets us use Swift’s key paths feature to access both our wrapper itself, and the underlying value that’s being wrapped. The only requirement is that the enclosing type needs to be a class, since the above subscript uses ReferenceWritableKeyPath, which relies on reference semantics.

When implementing the above API, we likely also want to prevent our property from being mutated using value semantics (since we want all mutations to go through our subscript), which can be done by marking the standard wrappedValue property as unavailable — like this:

@propertyWrapper
struct EnclosingTypeReferencingWrapper<Value> {
    static subscript<T>(
        _enclosingInstance instance: T,
        wrapped wrappedKeyPath: ReferenceWritableKeyPath<T, Value>,
        storage storageKeyPath: ReferenceWritableKeyPath<T, Self>
    ) -> Value {
        ...
    }
    
    @available(*, unavailable,
    message: "This property wrapper can only be applied to classes"
)
var wrappedValue: Value {
    get { fatalError() }
    set { fatalError() }
}
}

Note how we have to give wrappedValue both a getter and a setter, since otherwise the compiler will treat our property wrapper as immutable.

With the above setup in place, it’s now impossible to use our new property wrapper within a struct, and the compiler will automatically display the error message we defined using the @available attribute if we ever try to do so.

Reimplementing the Published property wrapper

To take a look at one type of situation in which the above pattern could be incredibly useful, let’s see if we can actually reimplement Combine’s Published property wrapper, which is often used in combination with the ObservableObject protocol to connect a class to a SwiftUI view.

While Combine is a closed-sourced framework that’s developed internally at Apple (so I haven’t actually seen its source code), we can make a few informed guesses as to how its Published wrapper is implemented based on the above subscript. Since each ObservableObject is required to have an objectWillChange publisher (which is automatically synthesized), the Published type likely calls that publisher in order to notify each observer of any changes — which might look something like this:

@propertyWrapper
struct Published<Value> {
    static subscript<T: ObservableObject>(
        _enclosingInstance instance: T,
        wrapped wrappedKeyPath: ReferenceWritableKeyPath<T, Value>,
        storage storageKeyPath: ReferenceWritableKeyPath<T, Self>
    ) -> Value {
        get {
            instance[keyPath: storageKeyPath].storage
        }
        set {
            let publisher = instance.objectWillChange
// This assumption is definitely not safe to make in
// production code, but it's fine for this demo purpose:
(publisher as! ObservableObjectPublisher).send()

instance[keyPath: storageKeyPath].storage = newValue
        }
    }

    @available(*, unavailable,
        message: "@Published can only be applied to classes"
    )
    var wrappedValue: Value {
        get { fatalError() }
        set { fatalError() }
    }

    private var storage: Value

    init(wrappedValue: Value) {
        storage = wrappedValue
    }
}

Note how we’re using a separate storage property to store our Published type’s underlying value, rather than using wrappedValue, since we’d like that default property to remain unavailable.

What’s really interesting is that if we drop the above code into a SwiftUI project, everything is actually highly likely to just keep working, unless we’re doing things like converting Published instances into publishers using their projected values (which we haven’t yet added support for), and as long as all of our @Published properties are defined within ObservableObject-conforming types.

So although the above is hardly a completely accurate 1:1 reimplementation of its built-in counterpart, it definitely demonstrates just how powerful this type of functionality can be. But now, let’s actually use that functionality to build something useful.

Proxy properties

When using frameworks like UIKit and AppKit, it’s very common to want to hide certain subviews within their enclosing parent view, and to only enable those views to be mutated through specific APIs. For example, the following HeaderView has a title and an image property, but keeps the underlying views used to render those properties private, and then manually connects those pieces together:

class HeaderView: UIView {
    var title: String? {
        get { titleLabel.text }
        set { titleLabel.text = newValue }
    }

    var image: UIImage? {
        get { imageView.image }
        set { imageView.image = newValue }
    }

    private let titleLabel = UILabel()
    private let imageView = UIImageView()
    
    ...
}

The benefit of the above pattern is that it gives each view a much smaller API surface, which in turn makes it less likely that our views will be misused somehow (for example by configuring a subview in a way that the parent view wasn’t designed to handle). However, it also requires a fair amount of boilerplate, as we currently have to manually forward each property’s getter and setter to the underlying view that’s used for rendering.

This is another type of situation in which an enclosing type referencing property wrapper could be incredibly useful. Since the language mechanism used to access a wrapper’s enclosing instance is key path-based, we could build a Proxy wrapper that automatically syncs its wrapped value with one of its enclosing type’s key paths — like this:

@propertyWrapper
struct Proxy<EnclosingType, Value> {
    typealias ValueKeyPath = ReferenceWritableKeyPath<EnclosingType, Value>
    typealias SelfKeyPath = ReferenceWritableKeyPath<EnclosingType, Self>

    static subscript(
        _enclosingInstance instance: EnclosingType,
        wrapped wrappedKeyPath: ValueKeyPath,
        storage storageKeyPath: SelfKeyPath
    ) -> Value {
        get {
    let keyPath = instance[keyPath: storageKeyPath].keyPath
    return instance[keyPath: keyPath]
}
set {
    let keyPath = instance[keyPath: storageKeyPath].keyPath
    instance[keyPath: keyPath] = newValue
}
    }

    @available(*, unavailable,
        message: "@Proxy can only be applied to classes"
    )
    var wrappedValue: Value {
        get { fatalError() }
        set { fatalError() }
    }

    private let keyPath: ValueKeyPath

    init(_ keyPath: ValueKeyPath) {
        self.keyPath = keyPath
    }
}

With that new property wrapper in place, we could now remove the manually implemented getters and setters from our HeaderView, and simply annotate the properties that we’re looking to sync to our underlying views with @Proxy:

class HeaderView: UIView {
    @Proxy(\HeaderView.titleLabel.text) var title: String?
@Proxy(\HeaderView.imageView.image) var image: UIImage?

    private let titleLabel = UILabel()
    private let imageView = UIImageView()
    
    ...
}

That’s already quite nice, but it is a bit of a shame that we have to repeatedly reference the HeaderView type when constructing our key paths. It would be so much nicer if we could make the compiler infer that type based on which enclosing type that our properties are being defined in.

To make that happen, we’re going to have to use a little bit of “type system hacking”. First, let’s rename our Proxy property wrapper to AnyProxy:

@propertyWrapper
struct AnyProxy<EnclosingType, Value> {
    ...
}

Then, let’s define a protocol that’ll use a type alias to specialize AnyProxy using Self. We’ll then apply that protocol to all NSObject types (which includes all UIKit and AppKit views) using an extension:

protocol ProxyContainer {
    typealias Proxy<T> = AnyProxy<Self, T>
}

extension NSObject: ProxyContainer {}

With the above set of changes in place, we’ll now be able to omit the enclosing type when referencing our proxied key paths, since Proxy now refers to our new specialized version of the AnyProxy property wrapper:

class HeaderView: UIView {
    @Proxy(\.titleLabel.text) var title: String?
@Proxy(\.imageView.image) var image: UIImage?

    private let titleLabel = UILabel()
    private let imageView = UIImageView()
    
    ...
}

We now have a very concise, elegant way of defining proxied properties in a way that doesn’t require any manual syncing, which isn’t only neat from a syntax perspective, but also removes the risk of us making a mistake when writing those manual getters and setters. Really nice!

Of course, one tradeoff with our final solution is that whenever we wish to refer to Proxy, we now have to make the enclosing type conform to our ProxyContainer protocol. However, since that protocol doesn’t actually have any requirements, and since we could always fall back to using AnyProxy directly, that’s not a big issue.

Support Swift by Sundell by checking out this sponsor:

Building Mobile Apps at Scale

Building Mobile Apps at Scale: Based on learnings from scaling the development of the Uber app over four years — this free, 200-page book will help you overcome 39 of the most commonly faced challenges when building large iOS apps. The book is free to download for a limited time, so grab your copy now.

Conclusion

Although it might not yet be a fully baked language feature that we can rely on within production code (unless we’re willing to take the risk involved in using underscore-prefixed language features), the fact that Swift’s property wrappers do in fact support referencing their enclosing instance is incredibly powerful.

Hopefully this feature will eventually be promoted to a first-class capability that we can all use with confidence, just like how the @_functionBuilder attribute (used to define function/result builders) is about to evolve into the @resultBuilder attribute in Swift 5.4.

What do you think? What sort of property wrappers could the above set of patterns enable you to write? Let me know, along with your questions, comments or feedback, either via Twitter or email.

Thanks for reading!