10

I have a ForEach block and a Stepper embedded in a List view. The contents of the List view's first section is as follows:

ForEach(record.nodes.indices, id: \.self) { index in
    HStack {
        TextField("X", text: self.$record.nodes[index].xString)
        Spacer()
        Divider()
        TextField("Y", text: self.$record.nodes[index].yString)
        Spacer()
    }
}
Stepper("± node", onIncrement: {
    self.record.nodes.append(Node(x: 0, y: 0))
}, onDecrement: {
    self.record.nodes.removeLast()
})

The issue I am facing is that upon calling self.record.nodes.removeLast(), the application crashes with an Index out of range error. I've been trying to solve this for hours, but to no avail.

I originally used onDelete, however that produced the same issue.

The project can be found at https://github.com/jacobcxdev/Timekeeper, with this error happening in RecordDetailView.swift.

JacobCXDev
  • 417
  • 6
  • 16
  • Could `record.nodes` be empty? Are you seeing rows in the list? – Chris Nov 26 '19 at 23:22
  • @Chris It has 6 elements in -> 5 elements when removing one. – JacobCXDev Nov 27 '19 at 00:04
  • 1
    I answered on this in ['Fatal error: index out of range' when deleting bound object in view](https://stackoverflow.com/questions/58984109/fatal-error-index-out-of-range-when-deleting-bound-object-in-view/59007863#59007863). Please read there. – Asperi Nov 27 '19 at 05:27
  • @Asperi, thanks. There is a reason that I am using `.indices`, which is that I need to create a binding to `self.$record.nodes[index].xString` and `self.$record.nodes[index].xString`, and when using just `record.nodes` as the data for the `ForEach` with the closure parameter `node`, the binding doesn't work on `node.xString`. – JacobCXDev Nov 27 '19 at 09:50
  • @JacobCXDev I am still looking for a solution to this problem. Same reasoning as you, I need to create bindings in the loop. Did you ever find a solution? – Maxwell Omdal Sep 04 '20 at 20:43
  • 1
    @MaxwellOmdal I don't think so, in fact, I think I ended up abandoning the whole project because SwiftUI was too immature to do what I needed :P. – JacobCXDev Sep 07 '20 at 21:42

3 Answers3

15

Problem Description

The index out of range error occurs because init(_ data: Range<Int>, content: @escaping (Int) -> Content) initializer of ForEach does not allow us to modify the array dynamically. For that, we need to use init(_ data: Data, content: @escaping (Data.Element) -> Content) initializer as @Asperi explained in the comment. However, it does not work either in this situation, where bindings are nested, as @JacobCXDev clarified in the reply.

Workaround (I found a solution, see below)

Use custom bindings and an additional state. The custom bindings solve the issue of ForEach over nested bindings. On top of that, you need to modify a state after every touch on the custom binding to re-render the screen. The following is a simplified (untested) code sample.

@State private var xString: String

ForEach(record.nodes) { node in
    let xStringBinding = Binding(
        get: { node.xString },
        set: {
            node.xString = $0
            self.xString = $0
        }
    )
    TextField("X", text: xStringBinding)
}

Solution (added on July 10, 2020)

Just define a view struct for the children like the following.

ForEach(record.nodes) { node in
    NodeView(node: node)
}

struct NodeView: View {
    @ObservedObject var node: Node

    var body: some View {
        TextField("X", text: self.$node.xString)
    }
}

class Node: ObservableObject, Identifiable {
    @Published var xString: String
    let id: String = UUID().uuidString

    init(xString: String) {
        self.xString = xString
    }
}
Kousuke Ariga
  • 691
  • 9
  • 10
2

It works as the following codes.

 struct Node {
var xString: String = "x"
var yString: String = "y"
var x: Int
var y: Int
 }

 struct RecordsNode {
var nodes : [Node]
 }

struct ContentView: View {



@State var record: RecordsNode = RecordsNode(nodes: [Node(x: 11, y: 11)])

var body: some View {

    Group{
    ForEach(record.nodes.indices, id: \.self) { index in
        HStack {
            TextField("X", text: self.$record.nodes[index].xString)
            Spacer()
            Divider()
            TextField("Y", text: self.$record.nodes[index].yString)
            Spacer()
        }
    }
    Stepper("± node", onIncrement: {
        self.record.nodes.append(Node(x: 0, y: 0))
    }, onDecrement: {
        self.record.nodes.removeLast()
    })}

}}
E.Coms
  • 11,065
  • 2
  • 23
  • 35
  • Hi, I kept the `Record` class and just made the `nodes` property a property of type `RecordsNode` as described in your answer, however the error still occurs — do you know why this is? – JacobCXDev Nov 27 '19 at 09:47
0

I had a same problem i solve with adding if condition before ForEach.

if (!IsDeleting) {
  ...
  .OnDecrement{
     IsDeleting := true
   }
}

any better solution?

Ernist Isabekov
  • 1,205
  • 13
  • 20