Using ForEach loop with Binding causes index out of range when array shrinks (SwiftUI)
@State
does seem to not be able to handle this, but ObservableObject
works.
I do not claim to know why apart from my best guess, which is that @State
tries too hard to avoid redraws by anticipating what the user wants, but in so doing does not support this.
Meanwhile ObservableObject
redraws everything on each small change. Works.
class FlashcardData: ObservableObject {
@Published var textArray = ["A","B","C"]
func updateData() {
textArray = ["A","B"]
}
}
struct IndexOutOfRangeView: View {
@ObservedObject var viewModel = FlashcardData()
var body:some View {
VStack{
ForEach(viewModel.textArray.indices, id: \.self){ index in
TextView(text: self.$viewModel.textArray[index])
.padding()
}
Button(action: {
self.viewModel.textArray = ["A","B"]
}){
Text(" Shrink array ")
.padding()
}
}
}
}
struct TextView:View {
@Binding var text:String
var body:some View {
Text(text)
}
}
Finally got the ins and outs of that issue that I was experiencing myself.
The problem is architectural. It is 2 folds:
- You are making a copy of your unique source of truth. ForEach loops Textfield but you are passing a copy through Binding. Always work on the single source of truth
- Combined with ForEach ... indices is supposed to be a constant range (hence the out of range when you remove an element)
The below code works because it loops through the single source of truth without making a copy and always updates the single source of truth. I even added a method to change the string within the subview since you originally passed it as a binding, I imagine you wanted to change it at some point
import SwiftUI
class DataSource: ObservableObject {
@Published var textArray = ["A","B","C"]
}
struct Test: View {
@EnvironmentObject var data : DataSource
var body:some View {
VStack{
ForEach(self.data.textArray , id: \.self) {text in
TextView(text: self.data.textArray[self.data.textArray.firstIndex(where: {text == $0})!])
.padding()
}
//Array modifying button
Button(action: {
self.data.textArray.removeLast()
}){
Text(" Shrink array ")
.padding()
}
}
}
}
struct TextView:View {
@EnvironmentObject var data : DataSource
var text:String
var body:some View {
VStack {
Text(text)
Button(action: {
let index = self.data.textArray.firstIndex(where: {self.text == $0})!
self.data.textArray[index] = "Z"
}){
Text("Change String ")
.padding()
}
}
}
}
#if DEBUG
struct test_Previews: PreviewProvider {
static var previews: some View {
Test().environmentObject(DataSource())
}
}
#endif