In SE-0258 there is mentioned a future direction of the feature - namely Referencing the enclosing 'self' in a wrapper type.
This future direction is already implemented in a preview version in the Swift toolchain that is part of the latest Xcode 11 betas.
This feature is already used in Combine as part of the mechanics used to synthesize the objectWillChange
of an ObservableObject
when @Published
is used.
Currently I cannot link to the documentation for ObservableObject
on Apple's developer site (can't find it anymore) but the synthesis was at least previously mentioned in the documentation and later elaborated on by @Douglas_Gregor in this thread on Twitter.
The implementation of the current state of the feature can be found here.
@Douglas_Gregor later mentioned that other use cases for the above mentioned 'future directions' was welcomed since the only one currently mentioned is the exact use case for the @Published
property wrapper.
I've only played very little with the hidden
feature (it was a bit hard to dig out even with the hints on Twitter ), but after a recent twitter chat with @Joe_Groff, I realized that this feature will actually enable a use case that I have wished for since I first started playing with property wrappers:
Consider a NoSQL database like the Firebase Real-time Database. Entities in this database can be considered to reside at a path of keys in a key-value tree. An example in the Firebase documentation is the model of a collection of chatrooms. Each chat room could have a list of messages and perhaps some configuration. A model of this in a key-value store could look like:
{
chatrooms: {
firechat: {
configuration: {
name: "FireChat"
},
messages: { ... }
},
swiftchat: {
configuration: {
name: "Swift chat"
},
messages: { ... }
},
... other chat rooms ...
}
}
Here the keys firechat
and swiftchat
are identifiers of individual chat rooms that each have a similar structure, namely some configuration entity and some collection of messages.
Now consider a property wrapper that could be initialized with the path to an entity and then represent this entity as a model value that is kept in sync with the database. Let's call this wrapper FirebaseValue
. This could be used with the above example as:
@FirebaseValue(path: "/chatrooms/firechat/configuration")
var configuration: Configuration
Where Configuration
might be implemented as:
struct Configuration: Codable {
var name: String
}
This is all really great, but now consider if you wish to use a Configuration
from some view model that was parametrized with the identifier of a chat room:
class ViewModel {
let chatroomId: String
init(chatroomId: String) {
self.chatroomId = chatroomId
}
@FirebaseValue(path: "/chatrooms/\(self.chatroomId)/configuration")
var configuration: Configuration
}
This unfortunately can't work since the initializer of the property wrapper cannot be called lazily.
In the future we may be able to use property wrappers in local scope to do something like:
class ViewModel {
init(chatroomId: String) {
self._configuration = @FirebaseValue(path: "/chatrooms/\(chatroomId)/configuration") var configuration: Configuration
}
@FirebaseValue
var configuration: Configuration
}
I'm not 100% sure about the notation for this, but I don't think that it is very easy to read with the almost dual definition of the configuration variable. I'd much rather think of the path as part of the definition of the property like in the first example, but I'd also really, really like the dynamic aspect of being able to refer to another property as part of the initializer.
And now finally to the link to the Referencing the enclosing 'self' feature:
This feature can be used along with keypaths to properties on Self
(or alternatively closures from Self
to a value) in order to lazily initialize actual values from the keypaths on the first access to the property (since here we are passed the _enclosingInstance
and can use this to convert our keypaths to values.
A very simple proof-of-concept implementation of this (that only resolves a single keypath) is:
@propertyWrapper
struct ResolveKeypathLazily<Value, EnclosingSelf> {
private var stored: Value
private let aKeyPath: KeyPath<EnclosingSelf, String>
private var resolved: String?
public init(wrappedValue: Value, keyPath: KeyPath<EnclosingSelf, String>) {
self.stored = wrappedValue
self.aKeyPath = keyPath
}
var wrappedValue: Value {
get { fatalError() }
set { fatalError() }
}
public static subscript(
_enclosingInstance observed: EnclosingSelf,
wrapped wrappedKeyPath: ReferenceWritableKeyPath<EnclosingSelf, Value>,
storage storageKeyPath: ReferenceWritableKeyPath<EnclosingSelf, Self>
) -> Value {
get {
// Resolve the keypath
if observed[keyPath: storageKeyPath].resolved == nil {
let keyPath = observed[keyPath: storageKeyPath].aKeyPath
observed[keyPath: storageKeyPath].resolved = observed[keyPath: keyPath]
}
return observed[keyPath: storageKeyPath].stored
}
set {
// TODO: Also perform the resolution here.
observed[keyPath: storageKeyPath].stored = newValue
}
}
}
The PoC does not actually use the resolved value for anything - it's just shown as a demonstration that it can be done.
This can be used as follows:
class TestResolver {
var identifier: String
@ResolveKeypathLazily(keyPath: \TestResolver.identifier)
var hamster: Int = 1
init(identifier: String) {
self.identifier = identifier
}
}
This would look a bit nicer if the implementation of the static subscript using a generic parameter of the type would cause the compiler to be able to infer this generic parameter. @Joe_Groff mentioned on twitter that the final version of the feature might be able to drop a hint to the compiler about inferring the generic parameter in case this type is used in the static subscript.
If that were the case, the example could look like:
class TestResolver {
var identifier: String
@ResolveKeypathLazily(keyPath: \.identifier)
var hamster: Int = 1
init(identifier: String) {
self.identifier = identifier
}
}
In general this gives a mechanism to lazily resolve values on the enclosing self by using keypaths in the initializer of the property wrapper.
The FirebaseValue example could then look like (with clever use of string interpolation):
class ViewModel {
let chatroomId: String
init(chatroomId: String) {
self.chatroomId = chatroomId
}
@FirebaseValue(path: "/chatrooms/\(\.chatroomId)/configuration")
var configuration: Configuration
}
There is however one caveat:
The lazy values can currently only be resolved once the 'wrappedValue' is accessed.
In case the projection is accessed first, then the values won't be resolved at this time.
So in order to make it possible to use keypaths (or closures) to model lazy values in the initializer for property wrappers, I propose adding a static subscript similar to the one for the wrappedValue with enclosingInstance, but for the projection instead.
Does anyone feel like this is a useful use case, and that it migth be a useful feature to add to the excellent property wrapper feature?