2

I'm trying to bind an array of strings into their corresponding text fields in a scrolling list. The number of rows is variable, and corresponds to the number of elements in the string array. The user can add or delete rows, as well as changing the text within each row.

The following Playground code is a simplified version of what I'm trying to achieve.

import SwiftUI
import PlaygroundSupport


struct Model {
    struct Row : Identifiable {
        var textContent = ""
        let id = UUID()
    }
    var rows: [Row]
}


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) {
                ElementCell(row: $0)
            }
        }
    }
}


struct ContentView: View {
    @State var model = Model(rows: (1...10).map({ Model.Row(textContent:"Row \($0)") }))
    var body: some View {
        NavigationView {
            ElementList(model: $model)
        }
    }
}


PlaygroundPage.current.liveView = UIHostingController(rootView: ContentView())

The issue is that I can't seem to get the "cell" to bind correctly with its corresponding element. In the example code above, Xcode 11.1 failed to compile it with error in line 26:

error: Text-Field Row.xcplaygroundpage:26:13: error: cannot invoke initializer for type 'ForEach<_, _, _>' with an argument list of type '(Binding<[Model.Row]>, @escaping (Binding<Model.Row>) -> ElementCell)'
            ForEach($model.rows) {
            ^

Text-Field Row.xcplaygroundpage:26:13: note: overloads for 'ForEach<_, _, _>' exist with these partially matching parameter lists: (Data, content: @escaping (Data.Element) -> Content), (Range<Int>, content: @escaping (Int) -> Content)
            ForEach($model.rows) {
            ^

What would be the recommended way to bind elements that are a result of ForEach into its parent model?

adib
  • 8,285
  • 6
  • 52
  • 91
  • 1
    I'm also suffering with this. One solution that some people give is to use indices like this: `ForEach (self.model.rows.indices, id:\.self) { index in ElementCell(row: self.$model.rows[index]) }` But this seems to have a fundamental problem: if the amount of items in your array gets reduced then Swift will throw a fatal error saying that the index is out of range. I would love to hear a solution that will get a dynamic binding working through a ForEach statement! – Tom Millard Oct 14 '19 at 08:34
  • 3
    @TomMillard Seems like the "solution" to this is to use classes as the model – **not** structs — like how Apple do it in its _Landmarks_ example project. In turn the row-models need to be in a @Published array property of the model object. Probably the row-model needs to be `@ObservableObject` classes as well. – adib Oct 18 '19 at 02:11
  • This is a fixed in Xcode 13 and runs just fine. See also: https://stackoverflow.com/a/67892081/2616297 – Damiaan Dufaux Jul 22 '21 at 06:12

0 Answers0