WritableKeyPath to an Optional

Consider this piece of code

struct B {
    var data: Int
}
struct A {
    var data: Int
    var b: B?
}

func test() {
    var a = A(data: 1, b: B(data: 1))
    a.b?.data = 3              // This works well
    a[keyPath: \.b?.data] = 3  // Cannot assign through subscript: key path is read-only
    let kp = \A?.?.data        // Only way to get a KeyPath<Root, Int?>, cannot be Writable
}

My goal is to be able to create a WritableKeyPath for something optional. Mostly, you can do something like this in a @Binding in SwiftUI (as it's dynamic in nature), but you cannot do it in a KeyPath. Now, the obvious solution is to add a mandatory unwrap (!), but this means less safeguarded code because Optionals cannot be used as writable reference.

This is obviously a simplified example, but my piece of code is about updating objects deep in an hierarchy that contains optionals - it resolves either to \Project.zones (Zone type) or \Project.zones[zoneIndex].asPageZone?.zones (Zone? type). I wanted to use WritableKeyPath for this, so I have a generic way to update a zone anywhere in the project, including sub-zones, but it currently doesn't seem possible. So I changed my asPageZone? to asPageZone!, and this works well, but I try to have safe code, and I feel bad doing this.

Anything obvious I'm missing?

Otherwise, I'm proposing it should be possible to do an optional keyPath write.

Also, it should be possible to create a wrapped optional version of a non-optional keyPath without resorting to an optional Root type (see kp variable, as the only workaround I know).

1 Like
1 Like

IMO, even the fact that a.b?.data = 3 works is weird. But if you really want this, you can create you own KeyPath-like entity, and then will look something like this:

OptionalKeyPath(\Project.zones[zoneIndex].asPageZone).chain(\.zones)
1 Like

TY for link.

Sad it got decided to be dropped. With SwiftUI proposing to mostly use struct for models, and having to dig deep in hierarchies, the fact it's harder to path a writable when there's an optional asks for either dangerous code with forced unwraps in keypaths, or dropping keypaths for an equivalent structure, like Mr. Pokhylets wrote here.

That's a good option, but then, it would disallow being linked to SwiftUI through a WritableKeyPath to Binding link:

$project[dynamicMember: keyPath]

This requires a writable key path. As long as I'm in my model, I prefer to use pure Swift and KeyPaths, but once I go in the views, I convert elements to SwiftUI, and this is harder to be done.

But I agree it's a good possibility. Ty for this.