0

For some reason I get an index out of bounds error when using state (with an array) and binding with one of its values. In general there is no problem adding more values to the array. However when you try and delete a value, you get an index out of bound error.

This is a simplified version the problem I have in my own project.

Try the sample below in SwiftUI. Simply hold one of the circle to try and delete one! When it deletes there will be a Swift error: Fatal error: Index out of range: file /Library/Caches/com.apple.xbs/Sources/swiftlang/swiftlang-1103.2.25.8/swift/stdlib/public/core/ContiguousArrayBuffer.swift, line 444

I believe the error comes from the fact that the value being deleted is being bound by one of the CustomView's value. On deletion the view no longer has access to that value, triggering the out of bounds error.

import SwiftUI

struct Test: View {
    @State var values: [Int] = [0, 1, 1, 1]
    var totalBalls: Int {
        return values.count
    }
    var body: some View {
        HStack {
            Text("\(totalBalls)")
        VStack {
            ForEach(0..<values.count, id: \.self) { i in
                CustomView(value: self.$values[i])
            }
            .onLongPressGesture {
                self.values.removeLast() //this line causes an error!
            }
        }
        }
    }
}

struct CustomView: View {
    @Binding var value: Int
    var body: some View {
        ZStack {
            Circle()
            Text("\(value)").foregroundColor(Color.orange)
        }.onTapGesture {
            self.value+=1
        }
    }
}

struct Test_Previews: PreviewProvider {
    static var previews: some View {
        Test()
    }
}

Matt Bart
  • 809
  • 1
  • 7
  • 26
  • Replace your `ForEach` with ForEach(Array(values.enumerated()), id: \.1.self) { (i, val) in } – Enes Karaosman May 10 '20 at 02:45
  • Interesting this fix only fixes the issue sometimes... For some reason it typically works on the first two deletions but fails on the third one. @EnesKaraosman – Matt Bart May 10 '20 at 02:55

1 Answers1

2

There are two reasons in this case: constant ForEach, and refresh racing with direct biding.

Here is a solution that fixes crash and works as expected. Tested with Xcode 11.4 / iOS 13.4.

struct TestDeleteLast: View {
    @State var values: [Int] = [0, 1, 1, 1]
    var totalBalls: Int {
        return values.count
    }
    var body: some View {
        HStack {
            Text("\(totalBalls)")
        VStack {
            // use index as id in ForEach
            ForEach(Array(values.enumerated()), id: \.0.self) { i, _ in
                CustomView(value: Binding(   // << use proxy binding !!
                    get: { self.values[i] },
                    set: { self.values[i] = $0 }))
            }
            .onLongPressGesture {
                self.values.removeLast()
            }
        }
        }
    }
}
Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
Asperi
  • 228,894
  • 20
  • 464
  • 690
  • 1
    This is seriously helpful, I'm surprised that I haven't seen this anywhere else. Two questions: 1. Is there a specific reason, I need to change my for loop? It seems to work fine as is? 2. Is the direct binding problem a bug or is this the expected behavior and I'm missing something? Thanks! – Matt Bart May 11 '20 at 00:56
  • It does work as it is, however when I set the `CustomView` as a destination of `NavigationLink` it says `FatalError: index out of range`. @Asperi – Faruk May 12 '20 at 02:05
  • @Faruk do you mind opening a question and possibly I can take a look at it> – Matt Bart May 24 '20 at 18:06
  • @MattBart I have opened a question a while ago and find a working solution by merging different answers on the site. However, you can check the answer of mine and the question as well and if there is any better way to do which you can think of, you can share. https://stackoverflow.com/a/61786442/1404324 – Faruk May 24 '20 at 18:09