Home > OS >  "Thread 1: Fatal error: Index out of range" when removing from array with @Binding
"Thread 1: Fatal error: Index out of range" when removing from array with @Binding

Time:03-04

I have run into this issue in SwiftUI. I want to be able to remove an item from an Array when the user presses on a button, but I get a "Thread 1: Fatal error: Index out of range" error when I try. This seems to have to do with the fact that IntView takes in a @Binding: if I make num just a regular variable, the code works fine with no errors. Unfortunately, I need to be able to pass in a Binding to the view for my purposes (this is a simplified case), so I am not sure what I need to do so the Binding doesn't cause the bug.

Here is my code:

import SwiftUI

struct IntView: View {
    
    @Binding var num: Int // if I make this "var num: Int", there are no bugs
    
    var body: some View {
        Text("\(num)")
    }
}

struct ArrayBugView: View {
    
    @State var array = Array(0...10)
    var body: some View {
        ForEach(array.indices, id: \.self) { num in
            IntView(num: $array[num])
            
            Button(action: {
                self.array.remove(at: num)
            }, label: {
                Text("remove")
            })
            
        }
    }
}

Any help is greatly appreciated!

CodePudding user response:

In your code the ForEach with indicies and id: \.self is a mistake. The ForEach View in SwiftUI isn’t like a traditional for loop. The documentation of ForEach states:

/// It's important that the `id` of a data element doesn't change, unless
/// SwiftUI considers the data element to have been replaced with a new data
/// element that has a new identity.

This means we cannot use indices, enumerated or a new Array in the ForEach. The ForEach must be on the actual array of identifiable items. This is so SwiftUI can track the row Views moving around, which is called structural identity and you can learn about it in Demystify SwiftUI WWDC 2021.

So you have to change your code to something this:

import SwiftUI

struct Item: Identifiable {
    let id = UUID()
    var num: Int
}

struct IntView: View {
    
    let num: Int
    
    var body: some View {
        Text("\(num)")
    }
}

struct ArrayView: View {
    
    @State var array: [Item] = [Item(num:0), Item(num:1), Item(num:2)]

    var body: some View {
        ForEach(array) { item in
            IntView(num: item.num)
            
            Button(action: {
                if let index = array.firstIndex(where: { $0.id == item.id }) {
                    array.remoteAt(index) 
                }
            }, label: {
                Text("remove")
            })
            
        }
    }
}
  • Related