Matt Bart
Matt Bart

Reputation: 939

Binding Value Source Deleted

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()
    }
}

Upvotes: 0

Views: 497

Answers (1)

Asperi
Asperi

Reputation: 257493

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()
            }
        }
        }
    }
}

Upvotes: 2

Related Questions