1

i have a small swiftUI programm in Xcode which let me create and delete Users in a list with a stepper to count points of the users.

everything works fine (adding users, renaming users, stepper counting) unless the deletion of the user.

it throws an 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 2020-05-23 12:06:22.854920+0200 Counter[21328:1125981] 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

Here is the code:

import SwiftUI

struct ContentView : View {
    @State var isEditing = false
    @State var stepperWerte = [3, 5, 7, 9]
    @State var editText = ["Player 1", "Player 2", "Player 3", "Player 4"]

    var startName = "new Player"
    var startLeben = 5

    var body: some View {
        NavigationView {
                List() { 
                    ForEach(0..<editText.count, id: \.self) {
                        spieler in HStack {
                            if self.editText.indices.contains(spieler) {
                            Stepper(value: self.$stepperWerte[spieler], in: -1...10, step: 1, label: {
                                TextField("", text: self.$editText[spieler], onEditingChanged: {_ in }, onCommit: {self.saveText(id: spieler, Text: self.editText[spieler])} )
                                .layoutPriority(1)
                                .fixedSize(horizontal: true, vertical: false)
                                Text("\(self.stepperWerte[spieler]) - \(spieler) - \(self.editText.count)")})
                            }
                        }
                    }
                    .onDelete(perform: spielerLoeschen)
                    .frame(width: nil, height: nil, alignment: .trailing)
                }
            .navigationBarTitle(Text("Nav_Title"))
            .navigationBarItems(leading: Button(action: { self.isEditing.toggle() }) { Text(isEditing ? "Done" : "Edit").frame(width: 85, height: 40, alignment: .leading) }, 
                                trailing: Button(action: spielerHinzufuegen, label: { Image(systemName: "plus") }) )
            .environment(\.editMode, .constant(self.isEditing ? EditMode.active : EditMode.inactive)).animation(Animation.spring())
        }
    }

    func spielerLoeschen(at offsets: IndexSet) {
        stepperWerte.remove(atOffsets: offsets)
        editText.remove(atOffsets: offsets)
    }
    func spielerHinzufuegen() {
        stepperWerte.append(startLeben)
        editText.append(startName)
    }
    func saveText(id: Int, Text: String) {
        editText[id] = Text
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

(ignore the "if" after the HStack, it has no real effect and those extra prints in the last Text to show the index and the total count)

if i dump the arrays (stepperWerte and editText) they are removed the right way -> the player selected for deletion will be removed correctly from the two arrays.

if i change

TextField("", text: self.$editText[spieler]

to

TextField("", text: self.$editText[0]

it works (unless naturally it displays the first player in all rows and i got the same error after deleting all the players (=rows))

any help would be great - thank you!

ma2412
  • 135
  • 7
  • See for problem explanation and solution [here](https://stackoverflow.com/a/58911168/12299030) – Asperi May 23 '20 at 10:26

2 Answers2

1

According to @Asperi i have changed my code to the following: import SwiftUI

struct BetterTextField : View {
    var container: Binding<[String]>
    var index: Int
    @State var text: String

    var body: some View {
        TextField("", text: self.$text, onCommit: {
            self.container.wrappedValue[self.index] = self.text
        })
        .layoutPriority(1)
        .fixedSize(horizontal: true, vertical: false)
    }
}

struct ContentView : View {
    @State var isEditing = false
    @State var stepperWerte = [3, 5, 7, 9]
    @State var editText = ["Player 1", "Player 2", "Player 3", "Player 4"]

    var startName = "new Player"
    var startLeben = 5

    var body: some View {
        NavigationView {
                List() { 
                    ForEach(0..<editText.count, id: \.self) {
                        spieler in HStack {
                            if self.editText.indices.contains(spieler) {
                            Stepper(value: self.$stepperWerte[spieler], in: -1...10, step: 1, label: {
                                BetterTextField(container: self.$editText, index: self.editText.firstIndex(of: self.editText[spieler])!, text: self.editText[spieler])
                                Text("\(self.stepperWerte[spieler]) - \(spieler) - \(self.editText.count)")})
                            }
                        }
                    }
                    .onDelete(perform: spielerLoeschen)
                    .frame(width: nil, height: nil, alignment: .trailing)
                }
            .navigationBarTitle(Text("Nav_Title"))
            .navigationBarItems(leading: Button(action: { self.isEditing.toggle() }) { Text(isEditing ? "Done" : "Edit").frame(width: 85, height: 40, alignment: .leading) }, 
                                trailing: Button(action: spielerHinzufuegen, label: { Image(systemName: "plus") }) )
            .environment(\.editMode, .constant(self.isEditing ? EditMode.active : EditMode.inactive)).animation(Animation.spring())
        }
    }

    func spielerLoeschen(at offsets: IndexSet) {
        stepperWerte.remove(atOffsets: offsets)
        editText.remove(atOffsets: offsets)
    }
    func spielerHinzufuegen() {
        stepperWerte.append(startLeben)
        editText.append(startName)
    }
    func saveText(id: Int, Text: String) {
        editText[id] = Text
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

... and it works - thank you!

but: is this a bug in SwiftUI or intentional?

ma2412
  • 135
  • 7
0

The problem is that you are not using your items directly in the ForEach loop. Consider using structs for your data as objects and make them identifiable.

struct Player : Identifiable {
    let id = UUID()
    var stepperWerte: Int
    var editText : String
}

struct ContentView : View {
    @State var isEditing = false

    @State var players = [Player(stepperWerte: 3, editText: "Player 1"), Player(stepperWerte: 5, editText: "Player 2"), Player(stepperWerte: 7, editText: "Player 3"), Player(stepperWerte: 9, editText: "Player 4")]

    var startName = "new Player"
    var startLeben = 5

    var body: some View {
        NavigationView {
                List() {

                    ForEach(self.players) { player in
                        SecondView(player: player)
                    }
                    .onDelete(perform: spielerLoeschen)
                }
                .frame(width: nil, height: nil, alignment: .trailing)
                .navigationBarTitle(Text("Nav_Title"))
                .navigationBarItems(leading: Button(action: { self.isEditing.toggle() }) { Text(isEditing ? "Done" : "Edit").frame(width: 85, height: 40, alignment: .leading) },
                                trailing: Button(action: spielerHinzufuegen, label: { Image(systemName: "plus") }) )
            .environment(\.editMode, .constant(self.isEditing ? EditMode.active : EditMode.inactive)).animation(Animation.spring())
        }
    }

    func spielerLoeschen(at offsets: IndexSet) {
        players.remove(atOffsets: offsets)
    }
    func spielerHinzufuegen() {
        players.insert(Player(stepperWerte: 4, editText: "Neuer Player"), at: 0)
    }
}

struct SecondView : View {

    var player : Player

    @State var stepperWerte : Int
    @State var name : String

    init(player : Player)
    {
        self._stepperWerte = State(initialValue: player.stepperWerte)
        self._name = State(initialValue: player.editText)

        self.player = player
    }


    var body: some View
    {
        Stepper(value: self.$stepperWerte, in: -1...10, step: 1, label: {
            TextField("", text: self.$name)
            .layoutPriority(1)
            .fixedSize(horizontal: true, vertical: false)
            Text("\(player.stepperWerte)")
            })
    }

}

I created a struct Player, and then an array of many Players. In the ForEach you can directly use players as Player confirms to Identifiable protocol. This is way easier as you can access a player object in your ForEach loop and you do not have to access everything with indices. In the deleting function you just delete the object out of the array or add something new to it. Deleting now works fine.

I have removed some code from the list row, just to reproduce it easier, just if you are wondering.

davidev
  • 7,694
  • 5
  • 21
  • 56
  • This also works fine and i was choosing this one as it was easier to understand for me. Thank you! – ma2412 May 23 '20 at 11:08
  • somehow, the editing of the player names and the values of the stepper are not changing/saving. does this work in your version? – ma2412 May 23 '20 at 11:35
  • @ma2412 I have updated the code. Add works, and name changing. I outsourced it to a second view and changed the struct. Sollte funktionieren ;) – davidev May 23 '20 at 12:12