ReferenceWritableKeyPath to a struct property (like SwiftUI’s @State)

I don’t have a concrete use case, I’m just trying to understand something here.

Short question: How does @State turn a key path to a struct property from a WritableKeyPath into a ReferenceWritableKeyPath?


Long question:

A key path to a struct’s property that is declared as var is a WritableKeyPath, e.g.:

struct MyStruct {
    var i: Int
}

print(type(of: \MyStruct.i)) // WritableKeyPath<MyStruct, Int>

Adding a property wrapper doesn’t change this:

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

struct MyStruct {
    @MyWrapper var i: Int
}

print(type(of: \MyStruct.i)) // WritableKeyPath<MyStruct, Int>

But if that property wrapper is SwiftUI’s @State, the key path becomes a ReferenceWritableKeyPath:

struct MyStruct {
    @State var i: Int
}

print(type(of: \MyStruct.i)) // ReferenceWritableKeyPath<MyStruct, Int>

I looked at the definition of @State and tried various things in my custom property wrapper, but the key path never becomes a ReferenceWritableKeyPath with just my code.

Is this some hardcoded magic for SwiftUI or what am I missing here?

You have to make your custom property wrapper a class (which is a reference type) instead of a struct/enum.
\MyStruct.i desugars to \MyStruct._i.wrappedValue so the type of keypath depends on a type of _i

edit: Oh, wait. @State is a struct according to the autogenerated interface. Weeeeird. The thing changing the keypath type is nonmutating set on wrappedValue instead of the default mutating for structs

Ah, thanks for the hint about nonmutating set. I can confirm that:

@propertyWrapper struct MyWrapper {
    var wrappedValue: Int {
        get { 0 }
        nonmutating set { }
    }
}

struct MyStruct {
    @MyWrapper var i: Int
}

print(type(of: \MyStruct.i)) // ReferenceWritableKeyPath<MyStruct, Int>

Removing just the nonmutating keyword results in a WritableKeyPath again.

I’m sure somebody put a whole lot of thought into this and there are good reasons why it is the way it is. Now I just need to figure out these reasons :thinking:


Edit: What I’d come up with if put on the spot is: A struct property that can be set without mutating the struct itself must be something that “delegates” the mutation to some place behind a pointer, like the example by Rob Mayoff with the UnsafeMutablePointer in this post. Another option would be a struct that delegates its storage to a singleton like UserDefaults.
The crucial thing is that there must be a pointer involved somewhere, i.e. a reference to something else.

Therefore, key paths to properties of structs that have nonmutating setters are ReferenceWritableKeyPaths.

I’m not sure this is convincing, but that’s what I’d guess.

3 Likes

ReferenceWritableKeyPath is used for all key paths that can be set without mutating the base. This is most commonly true (in fact, dominantly so) for key paths that start with reference types, and in fact you can make an argument that it's only ever true when there's a type that's behaving essentially like a reference somewhere in the path. We decided that it would be better to have a misnomer in the general case than to use a much more complicated and less suggestive name that would be technically correct in all cases.

6 Likes

To clarify: Do you mean that the general case is a key path that starts at any arbitrary type and has something reference-ish like nonmutating set somewhere along the path? (As opposed to a more special case where a key path that starts with a reference type.)
And that the misnomer is then the “Reference” part of ReferenceWritableKeyPath because there doesn’t have to be a reference type anywhere in the path?

The general case is that it starts with an arbitrary type, yes, not necessarily a reference type, which the name somewhat implies. Note that you don't even need reference-like structs to get this; a key path like \SomeStruct.propertyOfClassType.anyProperty is a ReferenceWritableKeyPath because assigning it doesn't mutate the original struct.

1 Like

Got it, thanks for the explanation! :+1: