0

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!

greatxp117
  • 57
  • 11
  • 1
    your code works well for me, on macos 12.3-beta, using xcode 13.3-beta, targets ios 15 and macCatalyst 12. It may be different on older systems. – workingdog support Ukraine Mar 03 '22 at 02:00
  • See 2nd part of answer [here](https://stackoverflow.com/a/64514440/9607863). Does that solve the issue? – George Mar 03 '22 at 02:15
  • 1
    The first part of the answer is related to the second part. When you use `.self` with the `Binding`, the OS has to keep track of the elements of the array of `Binding` and not just `Int`. Add in to that the general rule that if you are deleting items from an `Array` supplying a `ForEach` that you are not tracking as an `Identifiable` object, you have mayhem. It may not crash every time, but it will crash regularly. – Yrb Mar 03 '22 at 02:22
  • i can't reproduce the bug from your code, so maybe you can try to prevent the bug by wrapping it with an `if`, like `if array.indices.contains(num) { array.remove(at: num) }` – meomeomeo Mar 03 '22 at 09:47
  • Good video but the part you need is near minute 33 https://developer.apple.com/wwdc21/10022 – lorem ipsum Mar 03 '22 at 12:25
  • Yes, it seems like Apple fixed this issue in iOS 15. If you need to support older versions, you can try this extension, but use at your own risk: https://blog.apptekstudios.com/2020/05/quick-tip-avoid-crash-when-using-foreach-bindings-in-swiftui/ – greatxp117 Mar 03 '22 at 23:53

1 Answers1

1

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")
            })
            
        }
    }
}
malhal
  • 26,330
  • 7
  • 115
  • 133
  • Ah ok. I did some research based off of your answer and it seems like this post addresses what you should actually do if you need to use @Binding: https://stackoverflow.com/questions/57340575/binding-and-foreach-in-swiftui – greatxp117 Mar 03 '22 at 23:46
  • From my understanding, the solution is only available on iOS 15 and above, so looks like I will have to update my project :( – greatxp117 Mar 03 '22 at 23:49
  • We only use @Binding when we need write access to the num in IntView. The code your shared didn’t require it. – malhal Mar 05 '22 at 08:06
  • 1
    Well yes, I stated in the problem that this was a simplified version so that it was easy to understand, my real use case is more complicated and requires @Binding – greatxp117 Mar 05 '22 at 22:51