Remco Poelstra
Remco Poelstra

Reputation: 985

How to construct key path for encapsulated Toggle accessing a collection

I'm trying to build a new SwiftUI View that encapsulates a Toggle. The Toggle accesses a collection so that it can show up in a mixed state. I'm having difficulties constructing the proper KeyPath.

I've build the view as follows:

struct CornersView<C>: View where C: RandomAccessCollection  {
    @Binding var sources: C
    let keyPath: KeyPath<C.Element, Binding<ApertureCorners>>


    var body: some View {
        Toggle(sources: sources, isOn: keyPath.appending(path: \.upperLeftActive), label: {})
    }
}

#Preview {
    @State var sources = [ApertureCornerTreatment(radius: 1, corners: [], chamfered: false)]

    return CornersView(sources: $sources, keyPath: \.corners)
}

This gives me the following error on the last line in the preview: Key path value type 'ApertureCorners' cannot be converted to contextual type 'Binding<ApertureCorners>'

Oddly enough, if I would want a Toggle on chamfered in the preview I would write: Toggle(sources: $sources, isOn: \.chamfered, label: {}), which looks very similar to the current preview.

What did I do wrong?

Upvotes: 0

Views: 76

Answers (1)

rob mayoff
rob mayoff

Reputation: 385970

In the future, please post a complete example. I've stubbed in some code to be able to reproduce your error, but you should have included all this in your post:

struct ApertureCorners: OptionSet {
    var rawValue: UInt8

    static let upperLeft = Self(rawValue: 1 << 0)

    var upperLeftActive: Bool {
        get { contains(.upperLeft) }
        set {
            if newValue { insert(.upperLeft) }
            else { remove(.upperLeft) } }
    }
}

struct ApertureCornerTreatment {
    var radius: CGFloat
    var corners: ApertureCorners
    var chamfered: Bool
}

With those definitions, I can reproduce your error:

struct CornersView<C>: View where C: RandomAccessCollection & MutableCollection {
    @Binding var sources: C
    let keyPath: KeyPath<C.Element, Binding<ApertureCorners>>

    var body: some View {
        Toggle(sources: sources, isOn: keyPath.appending(path: \.upperLeftActive), label: {})
    }
}

#Preview {
    @State var sources = [ApertureCornerTreatment(radius: 1, corners: [], chamfered: false)]

    return CornersView(
        sources: $sources,
        keyPath: \.corners
//               ^ πŸ›‘ Key path value type 'ApertureCorners' cannot be converted to contextual type 'Binding<ApertureCorners>'
    )
}

The location of the error isn't really where you have a problem, though.

The main problem is in your call to Toggle (line-wrapped for readability):

    Toggle(
        sources: sources,
        isOn: keyPath.appending(path: \.upperLeftActive),
        label: {}
    )

You're passing sources, not $sources. So you're passing a plain array (type [ApertureCornerTreatment]) to Toggle. The isOn argument therefore needs to be a KeyPath<ApertureCornerTreatment, Binding<Bool>>.

There are no Bindings in ApertureCornerTreatment or ApertureCorners, nor is there any obvious way to create one, so you're not going to be able to create a key path with that type.

The trick, which is not explained in Toggles documentation, is this: whatever you pass as the sources argument to Toggle needs to be not just a RandomAccessCollection, but also something that can create Bindings.

The only type I'm aware of that conforms to RandomAccessCollection and can create Bindings is… Binding itself, when its Value type is itself a RandomAccessCollection.

So, let's change the code to pass $sources to Toggle, and get two new errors:

    Toggle(
//  ^ πŸ›‘ Initializer 'init(sources:isOn:label:)' requires that 'C' conform to 'MutableCollection'
        sources: $sources,
        isOn: keyPath.appending(path: \.upperLeftActive),
//                    ^ πŸ›‘ Cannot convert value of type 'KeyPath<C.Element, Binding<Bool>>' to expected argument type 'KeyPath<Binding<C>.Element, Binding<Bool>>'
        label: {}
    )

We fix these errors by adding a requirement that C conform to MutableCollection, and by changing the type of the keyPath property:

struct CornersView<C>: View where C: RandomAccessCollection & MutableCollection {
//                                                 ADD THIS ^^^^^^^^^^^^^^^^^^^
    @Binding var sources: C
    let keyPath: KeyPath<Binding<C.Element>, Binding<ApertureCorners>>
//           CHANGE THIS ^^^^^^^^^^^^^^^^^^

    var body: some View {
        Toggle(
            sources: $sources,
            isOn: keyPath.appending(path: \.upperLeftActive),
            label: {}
        )
    }
}

With those changes, Swift accepts the #Preview.

Upvotes: 0

Related Questions