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?
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 Binding
s 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 Toggle
s 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 Binding
s.
The only type I'm aware of that conforms to RandomAccessCollection
and can create Binding
s 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
.