← All articles

Accessing the enclosing self inside a property wrapper

Small disclaimer

I should probably start with a warning: this is not fully public API and things might change in future Swift versions. That being said, it works great, and as long as you're ready to update your wrappers when the public API rolls in, there's no reason you shouldn't start using it today.

Why do we need this?

The enclosing self is the instance whose members are encased in property wrappers. A quick example:

class HeaderViewModel: ObservableObject {
    @Published var firstName: String = "Anakin"
}

For the @Published implementation, the enclosing self is HeaderViewModel.

Accessing the enclosing self is especially useful when we're sharing a context between property wrappers. For example, we could build a property wrapper that accesses a shared managedObjectContext on the enclosing self and would only work with subclasses of NSManagedObject. For those that don't use Core Data, NSManagedObject vends a managedObjectContext that can be used to fetch persistent data from the store, among other things.

class User: NSManagedObject {
    // The @Query implementation requires the
    // enclosing self to be a NSManagedObject
    // and has access to all its methods and
    // properties
    @Query(\.articles) var articles
    @Query(\.rating) var rating
}

Since the property wrapper API is already a bit convoluted, we won't be touching Core Data to avoid other complications. We'll build instead @Concatenate, a simple property wrapper that puts together multiple string members of the same entity and exposes the result as a String, or, as an array of strings through its projected value:

class User {
    let firstName: String
    let lastName: String
    @Concatenating(\User.firstName, \User.lastName) var fullName: String
}

let user = User(firstName: "Obi-Wan", lastName: "Kenobi")
print(user.fullName) // "Obi-Wan Kenobi"
print(user.$fullName) // ["Obi-Wan", "Kenobi"]

Let's see an example

Let's first see the whole implementation and then go through each of the interesting bits.

@propertyWrapper
struct Concatenating<EnclosingSelf> {
    private let keyPaths: [KeyPath<EnclosingSelf, String>]

    init(_ keyPaths: KeyPath<EnclosingSelf, String>...) {
        self.keyPaths = keyPaths
    }

    static subscript(
        _enclosingInstance instance: EnclosingSelf,
        wrapped wrappedKeyPath: ReferenceWritableKeyPath<EnclosingSelf, String>,
        storage storageKeyPath: ReferenceWritableKeyPath<EnclosingSelf, Self>
    ) -> String {
        get {
            let storage = instance[keyPath: storageKeyPath]
            return storage.keyPaths
                .map {
                    instance[keyPath: $0]
                }
                .joined(separator: " ")
        }
        @available(*, unavailable,
                   message: "@Concatenating wrapped value is readonly")
        set {}
    }

    static subscript(
        _enclosingInstance instance: EnclosingSelf,
        projected wrappedKeyPath: ReferenceWritableKeyPath<EnclosingSelf, [String]>,
        storage storageKeyPath: ReferenceWritableKeyPath<EnclosingSelf, Self>
    ) -> [String] {
        get {
            let storage = instance[keyPath: storageKeyPath]
            return storage.keyPaths
                .map {
                    instance[keyPath: $0]
                }
        }
        @available(*, unavailable,
                   message: "@Concatenating projected value is readonly")
        set {}
    }

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

    @available(*, unavailable,
               message: "@Concatenating can only be applied to classes")
    var projectedValue: [String] {
        get { fatalError() }
        set { fatalError() }
    }
}

The first thing to notice, the property wrapper is now generic, with an EnclosingSelf parameter. This is resolved in the initializer as the Root of the keypath array.

struct Concatenating<EnclosingSelf> {
    private let keyPaths: [KeyPath<EnclosingSelf, String>]

    init(_ keyPaths: KeyPath<EnclosingSelf, String>...) {
        self.keyPaths = keyPaths
    }
// ...

Let's have a look at the static subscript method.

// ...
static subscript(
        _enclosingInstance instance: EnclosingSelf,
        wrapped wrappedKeyPath: ReferenceWritableKeyPath<EnclosingSelf, String>,
        storage storageKeyPath: ReferenceWritableKeyPath<EnclosingSelf, Self>
    ) -> String
// ...

To better understand each of the above parameters, here's the desugared snippet for the User class above.

class User {
    let firstName: String
    let lastName: String

    private var _fullName: Concatenating<User> = .init(\User.firstName, \User.lastName)
    var fullName: Int {
        get {
            Concatenating<User>[instanceSelf: self,
                                wrapped: \User.fullName,
                                storage: \User._fullName]
        }
        @available(*, unavailable,
                   message: "@Concatenating wrapped value is readonly")
        set {
            Concatenating<User>[instanceSelf: self,
                                wrapped: \User.fullName,
                                storage: \User._fullName] = newValue
        }
    }
}

Each User instance will have 2 synthesized properties, the _fullName private property that holds a Concatenating<User> struct initialized with the parameters we send to the property wrapper and an immutable fullName that uses Concatenating<User>'s static storage. Also, notice how the EnclosingSelf is resolved to User.

Going through each of the parameters sent to the static subscript:

  • _enclosingInstance is what we're looking for, the enclosing self, an instance of User
  • wrapped a keypath to the fullName property
  • storage a keypath to the _fullName property wrapper struct (Concatenating<User>)
Info:

It's called storage because it usually holds the actual value of fullName (e.g. in wrappedValue or other property of the Concatenating struct). In our case, it concatenates multiple values and returns the result: it's a stateless property wrapper.

The same goes for the projected value. Notice the projected keypath and its value [String]. The two method signatures are similar, I've added them both because I had trouble finding the latter in the docs (luckily it was in the Swift's unit tests).

static subscript(
        _enclosingInstance instance: EnclosingSelf,
        projected wrappedKeyPath: ReferenceWritableKeyPath<EnclosingSelf, [String]>,
        storage storageKeyPath: ReferenceWritableKeyPath<EnclosingSelf, Self>
    ) -> [String]

Finally, we mark as unavailable the wrappedValue and projectedValue properties. This is because, at the time of the writing, static subscript property wrappers are only synthesized for reference semantics entities, which means, value semantics entities like structs will attempt to use the wrappedValue/projectedValue instead.

Wrapping it up

// if we replace the class with a struct
struct User {
    let firstName: String
    let lastName: String
    @Concatenating(\User.firstName, \User.lastName) var fullName: String
    // ^ the compiler will complain here
}

In future Swift versions, the compiler might be able to pick between one of the two storage strategies (wrappedValue or static subscript) and this last step will not be required anymore.

Join us
Sign up and be the first to know when we release new content!