aneurinc
aneurinc

Reputation: 1238

SwiftUI: Deleting last row in ForEach

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.

Upvotes: 8

Views: 2824

Answers (3)

Asperi
Asperi

Reputation: 257663

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

Upvotes: 10

Adrien
Adrien

Reputation: 1917

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

Upvotes: 0

AlbertUI
AlbertUI

Reputation: 1517

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

Upvotes: 5

Related Questions