The root cause is that the thing you're binding to is the container type
Model
. To get (working) bindings to its contents, the $-prefixed expression must begin with that property. If you look at Apple's SwiftUI tutorial, this is one of the reasons why they lookup the index of each landmark inside their
UserData.landmarks
array—so they can use
$userData.landmarks[idx]
to generate the binding to the actual member of the array.
In your case, you realize you can't pass
model.rows
, because that'll just end up working on a copy of the real array. However, passing
$model.rows
doesn't work because that's a
Binding<[Model.Row]>
, which isn't the
RandomAccessCollection
that the
ForEach
view requires (this is the error being reported by the compiler). It can act like one thanks to its
DynamicMemberLookup
conformance, but there isn't a way to really express that as a Swift generic parameter yet.
Next, think about how SwiftUI works. The real
@State
value is living in
ContentView
. When that state value changes—and as a
struct
that includes changes to any of its contents—the
ContentView
's
body
will be re-fetched. This will also trigger calls for the bodies of each view that holds a binding to that
Model
state variable, so your
ElementList
's body will also be re-fetched. The rows won't necessarily, because they'll bind to an individual
String
inside an individual
Model.Row
inside the
Model
.
Now, with the
Model
being the thing at the root of the state tree, we need changes to the text inside each
Row
to properly affect the
@State
property inside your
ContentView
. That means that every binding must be generated from a binding that was generated from the state value itself: they need the 'parent' links back to the original state property.
That brings us back to that index argument in the first paragraph. Inside your
ForEach
builder block, you need an expression that begins by accessing
$model
—that instance knows how to create a binding to something at a key-path, and
$something.a.b.c
is actually translated into
_something[dynamicMember: \.a.b.c]
. This means that even if you iterate over the rows—i.e. with
ForEach(model.rows)
—you can't do anything with that actual
row
instance except use it to determine a means to reach inside the real collection inside
$model
. Apple's code uses a function to fetch the index of the object with a matching identifier, but here you have a shortcut since you're iterating over the collection directly: you can use
ForEach(model.rows.indices)
to invoke your view builder with an index into the row collection, and use
$model.rows[idx]
to bind to the correct instance of the row.
This gives the following working example:
struct Model {
struct Row: Identifiable {
var textContent = ""
let id = UUID()
}
var rows: [Row]
init<S: Sequence>(rowContents: S) where S.Element: StringProtocol {
self.rows = rowContents.map { Row(textContent: String($0)) }
}
}
struct ElementCell: View {
@Binding var row: Model.Row
var body: some View {
TextField("Field", text: $row.textContent)
}
}
struct ElementList: View {
@Binding var model: Model
var body: some View {
List {
ForEach(model.rows.indices) { idx in
return ElementCell(row: self.$model.rows[idx])
}
}
}
}
struct ContentView: View {
@State var model = Model(rowContents: (1...10).map { "Row \($0)" })
var body: some View {
NavigationView {
ElementList(model: $model)
}
}
}