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 ofUser
wrapped
a keypath to thefullName
propertystorage
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 struct
s 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.