8

I am trying to remove rows inside a ForEach. Removing the last row always throws an index out of range exception. Removing any other row does not.

ForEach(Array(player.scores.enumerated()), id: \.element) { index, score in
    HStack {
        if self.isEditSelected {
            Button(action: {
                self.player.scores.remove(at: index)
            }, label: {
                Image("delete")
            })
        }        
        TextField("\(score)", value: self.$player.scores[index], formatter: NumberFormatter())
    }
}

I have tried using ForEach(player.indices...) & ForEach(player.scores...), but see the same problem.

Looks to me like the crash happens here self.$player.scores[index], as hardcoding the index to any value other that the last row is working.

Does anyone know how to fix this? Or if there is a better approach.

1

3 Answers 3

10

Here is fix

ForEach(Array(player.scores.enumerated()), id: \.element) { index, score in
    HStack {
        if self.isEditSelected {
            Button(action: {
                self.player.scores.remove(at: index)
            }, label: {
                Image("delete")
            })
        }        
        TextField("\(score)", value: Binding(   // << use proxy binding !!
            get: { self.player.scores[index] },
            set: { self.player.scores[index] = $0 }), 
            formatter: NumberFormatter())
    }
}
Sign up to request clarification or add additional context in comments.

3 Comments

Brilliant. That's the best way to enumerate an Array while having a non constant range!
do you know @Asperi why the proxy binding prevents the view to try a redraw before the index is updated ? Is it a hack or a legit way of coding ?
@Asperi why it solves the issue? What was wrong... and why now it is correct?
5

Based on @Asperi answer

public extension Binding where Value: Equatable {
    static func proxy(_ source: Binding<Value>) -> Binding<Value> {
            self.init(
                get: { source.wrappedValue },
                set: { source.wrappedValue = $0 }
            )
    }
}

You can use this as follows:

TextField("Name", text: .proxy($variable))

Comments

0

Xcode 13.0 beta introduced a new way to establish two-way-bindings between the elements of a collection and the views built by ForEach / List. This method fixes the crash related to deleting the last row.

struct Score: Identifiable {
    let id = UUID()
    var value: Int
}

struct Player {
    var scores: [Score] = (1...10).map {_ in .init(value: Int.random(in: 0...25))}
}

struct BindingTest: View {
    @State private var player = Player()

    var body: some View {
        List {
            ForEach($player.scores) { $score in
                HStack {
                    TextField("\(score.value)", value: $score.value,
                        formatter: NumberFormatter())
                }
            }
            .onDelete { player.scores.remove(atOffsets: $0)}
        }
    }
}

Comments

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.