12

I have an app that

  1. Has a collection of editable items (like a bunch of notes)
  2. Need to bind these items to a child view that can edit each item (like a note editor)

But every time the array reduces in size, it causes an index out of range error that is not directly because of my code

As far as I know, it's because: after the loop refreshes with the changed array, the views it created before somehow isn't completely removed and still trying access the out of range part. But that's all I can figure out myself

Here is my sample code:

import SwiftUI


struct Test: View {
    @State var textArray = ["A","B","C"]

    var body: some View {
        VStack {
            ForEach(textArray.indices, id: \.self){ index in
                TextView(text: self.$textArray[index])
                    .padding()
            }

            //Array modifying button

            Button(action: {
                textArray = ["A","B"]
            }) {
                Text("Shrink array")
                    .padding()
            }
        }
    }
}

struct TextView: View {
    @Binding var text: String

    var body: some View {
        Text(text)
    }
}

Is there any better way to satisfy the two requirements above without causing this problem? Thank you.

1
  • I believe this is not an issue in iOS 15+ (Xcode 15). I didn't see any issue when I try to run in Xcode 15 Commented Nov 27, 2023 at 5:55

2 Answers 2

6

@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)
    }
}
Sign up to request clarification or add additional context in comments.

2 Comments

Thanks it does work for me. Guess I'll read more about ObservableObject
But wat about @FetchRequest and filtering results. it throws Index out of range
4

Finally got the ins and outs of that issue that I was experiencing myself.

The problem is architectural. It is 2 folds:

  1. 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
  2. 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

11 Comments

@Fabian Its not a copied answer. You can use @State var TextArray and its still works. i've edited his answer to make it clearer
Haha I take it back, I was confused due to viewModel.TextArray instead of textArray on its own. I wonder why it works though, do you have any idea?
I was confused at first too, but didn't want to edit someone else's answer to my preference. I have no idea why it works but it really did...
@Fabian observableObject works because it forces a redraw of the view when changed (just look at the definition in Xcode when you select this type) The issue arises because the ForEach expects a constant range to minimize the number of redraws. The observable object forces the ForEach to redraw (since its a view) and therefore with the updated range. I’m not sure about why proxy binding works but my best guess is that by explicitly setting the binding it forces ForEach to redraw.
@Fabien ObservableObject was the way to go you were right on this. It works because being the single source of truth it gets updated before the view and the view is aware of it when it redraws. However, in your answer you made a copy of it and although it might work, it might also cause issues or headaches later.
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.