UPGRADE YOUR SKILLS: Learn advanced Swift and SwiftUI on Hacking with Swift+! >>

Deleting an item from an array cause an index out of range in view

Forums > SwiftUI

Hi there, I'm getting an index out of range when I drag up the last rendered token in a token stack. Setting the amount to 0. It seems that the foreach in PlayerTokensView don't get the updated token array count while rendering but cannot access the last element becouse it don't exists anymore... I don't understand. Can someone help me? Thanks!

import Foundation
import SwiftUI

class Player: ObservableObject {

    let name: String
    @Published var tokens: [Token] = [
        Token(name: "Beast", types: [.creature], subTypes: ["Beast"], manaColors: [.green], power: 3, thoughness: 3, abilities: ["Trample"], amount: 3),
        Token(name: "Goblin", types: [.creature], subTypes: ["Goblin"], manaColors: [.red], power: 1, thoughness: 1, abilities: ["Haste"], amount: 5)
    ]

    // MARK: - INIT
    init(name: String) {
        self.name = name
    }

}

struct Token: Identifiable {

    // MARK: - PROPERTIES
    let id = UUID()
    let name: String
    let types: [CardType]
    let subTypes: [String]
    let manaColors: [ManaColor]
    let power: Int?
    let thoughness: Int?
    let abilities: [String]
    var amount = 1

}

struct StackItemTokenView: View {

    @ObservedObject var player: Player
    @Binding var token: Token
    @State var isAttacking = false

    var body: some View {
        TokenView(token: token)
            .offset(offset)
            .gesture(drag)
    }

    private var drag: some Gesture {
        DragGesture()
            .onEnded { gesture in
                if gesture.translation.height > 50 {
                    token.amount += 1
                } else if gesture.translation.height < -50 {
                    if token.amount == 1 {
                        let index = player.tokens.firstIndex { item in
                            item.id == token.id
                        }
                        if let index = index {
                            player.tokens.remove(at: index)
                        }
                    } else {
                        token.amount -= 1
                    }
                }
            }
    }

}

struct TokenStackView: View {

    @ObservedObject var player: Player
    @Binding var token: Token

    var body: some View {
        VStack {
            ZStack(alignment: .topLeading) {
                ForEach(numbers) { number in
                    StackItemTokenView(player: player, token: $token)
                        .padding(.top, CGFloat(number) * 20)
                        .padding(.leading, CGFloat(number) * 10)
                }
            }
        }
    }

    var numbers: [Int] {
        var numbers = [Int]()
        for number in 0 ..< token.amount {
            numbers.append(number)
        }
        return numbers
    }

}

struct PlayerTokensView: View {

    @ObservedObject var player: Player

    var body: some View {
        ScrollView(.horizontal, showsIndicators: true) {
            HStack(spacing: 0) {
                ForEach(player.tokens.indices) { index in
                    TokenStackView(player: player, token: $player.tokens[index])
                        .padding()
                }
            }
        }
    }

}

3      

The problem is that you are using this form of ForEach:

init(_ data: Range<Int>, content: @escaping (Int) -> Content)

According to Apple's docs, this version of ForEach is for constant data and "only reads the initial value of the provided data and doesn’t need to identify views across updates." So when you remove an item from players.tokens, you have changed the array but SwiftUI doesn't see that change and tries to use the original array.

If the data you are looping over with ForEach is meant to change, you need to use one of the other two forms of ForEach instead:

init(_ data: Data, content: @escaping (Data.Element) -> Content)
init(_ data: Data, id: KeyPath<Data.Element, ID>, content: @escaping (Data.Element) -> Content)

6      

Wow, thank you very much! I had come to the source of the problem but I couldn't find a solution... Thanks again

3      

Hacking with Swift is sponsored by Essential Developer

SPONSORED Join a FREE crash course for mid/senior iOS devs who want to achieve an expert level of technical and practical skills – it’s the fast track to being a complete senior developer! Hurry up because it'll be available only until April 28th.

Click to save your free spot now

Sponsor Hacking with Swift and reach the world's largest Swift community!

Fatal error: Index out of range: file Swift/ContiguousArrayBuffer.swift, line 444

Sorry, view works better now, it updates as soon as I add a token, but still get the above error when I delete a token. I have updated all ForEachs with your suggestion and I don't receive warnings anymore, but the out of range persists. May be I'm still missing something?

3      

What does your code look like after your changes?

3      

As you suggested I added the id: KeyPath and now code updates all necessary views. But still get the out of range error on delete. Maybe the error persists because I have to use OnDelete with indexsets? But I don't know how... I mean not calling it the usual way Thanks for your time

import Foundation
import SwiftUI

class Player: ObservableObject {

    let name: String
    @Published var tokens: [Token] = [
        Token(name: "Beast", types: [.creature], subTypes: ["Beast"], manaColors: [.green], power: 3, thoughness: 3, abilities: ["Trample"], amount: 3),
        Token(name: "Goblin", types: [.creature], subTypes: ["Goblin"], manaColors: [.red], power: 1, thoughness: 1, abilities: ["Haste"], amount: 5)
    ]

    // MARK: - INIT
    init(name: String) {
        self.name = name
    }

}

struct Token: Identifiable {

    // MARK: - PROPERTIES
    let id = UUID()
    let name: String
    let types: [CardType]
    let subTypes: [String]
    let manaColors: [ManaColor]
    let power: Int?
    let thoughness: Int?
    let abilities: [String]
    var amount = 1

}

struct StackItemTokenView: View {

    @ObservedObject var player: Player
    @Binding var token: Token
    @State var isAttacking = false

    var body: some View {
        TokenView(token: token)
            .offset(offset)
            .gesture(drag)
    }

    private var drag: some Gesture {
        DragGesture()
            .onEnded { gesture in
                if gesture.translation.height > 50 {
                    token.amount += 1
                } else if gesture.translation.height < -50 {
                    if token.amount == 1 {
                        let index = player.tokens.firstIndex { item in
                            item.id == token.id
                        }
                        if let index = index {
                            player.tokens.remove(at: index)
                        }
                    } else {
                        token.amount -= 1
                    }
                }
            }
    }

}

struct TokenStackView: View {

    @ObservedObject var player: Player
    @Binding var token: Token

    var body: some View {
        VStack {
            ZStack(alignment: .topLeading) {
                ForEach(numbers, id: \.self) { number in
                    StackItemTokenView(player: player, token: $token)
                        .padding(.top, CGFloat(number) * 20)
                        .padding(.leading, CGFloat(number) * 10)
                }
            }
        }
    }

    var numbers: [Int] {
        var numbers = [Int]()
        for number in 0 ..< token.amount {
            numbers.append(number)
        }
        return numbers
    }

}

struct PlayerTokensView: View {

    @ObservedObject var player: Player

    var body: some View {
        ScrollView(.horizontal, showsIndicators: true) {
            HStack(spacing: 0) {
                ForEach(player.tokens.indices, id: \.self) { index in
                    TokenStackView(player: player, token: $player.tokens[index])
                        .padding()
                }
            }
        }
    }

}

3      

You are not protecting for an empty set.

.firstIndex will return NSNotFound for an empty set.

Your code is

if token.amount == 1 {
   let index = player.tokens.firstIndex { item in
      item.id == token.id
   }
   if let index = index {
      player.tokens.remove(at: index)
   }
} else {
   token.amount -= 1
}

What happens when you have token.amount = 1, is that you remove player.tokens at the index firstIndex leaving an empty set. Also token.amount remains unchanged, and is 1.

In this code, and / or possibly elsewhere you need to protect for an empty set

for example consider

if !player.tokens.isEmpty {
   if token.amount == 1 {
      let index = player.tokens.firstIndex { item in
         item.id == token.id
      }
      if let index = index {
         player.tokens.remove(at: index)
      }
   } else {
      token.amount -= 1
   }
}

3      

@Greenamberred, thanks for your reply, the error persists. And that because I think I never get an empty set because I always check and delete after. The error is raised in the view I think within the foreach, it isn't able to 'see' the modified array.

3      

But maybe your code solves another hidden bugs I didn't notice, think so

I found a workaround by not deleting from the array but rendering only tokens wich have an amount > 0... It works but is a workaround anyway : /

3      

It's crashing because of this:

ForEach(player.tokens.indices, id: \.self) { index in
    TokenStackView(player: player, token: $player.tokens[index])
        .padding()
}

when you remove the last item in the player.tokens array. Even though the empty array indicies is 0..<0, it looks like the ForEach is trying to call TokenStackView with an index of 0, which doesn't exist and causes the crash.

I'm pretty sure it's because of the Binding for the token parameter. I'm thinking maybe you should rethink how you are passing data around.

3      

Thanks again, but that's not the case, because the view crashes even when I delete NOT the last element, in which case the array has at least one element. But yes, maybe I have to rewrite the data stack

3      

Hmm, interesting. Removing the last remaining item in the array is the only time I saw a crash. And I was able to cause the same crash in a simpler, stripped-down example too.

Can you post your TokenView? I had to make a simple View (basically just a single Text element) since you hadn't posted the actual one and just maybe there's something there causing the issue.

3      

Here it is...

//
//  TokenView.swift
//  MtG Manager
//
//  Created by Alessandro on 30/03/2021.
//

import SwiftUI

struct TokenView: View {

    @StateObject var model = Model()
    var token: Token

    var body: some View {
        VStack {
            Text(token.name)
                .fontWeight(.semibold)
            VStack {
                ForEach(token.types) { type in
                    Text(type.rawValue.capitalized)
                }
            }
            .font(.footnote)
            .padding(.vertical, 4)
            ForEach(token.abilities, id: \.self) { ability in
                Text(ability)
            }
            .font(.footnote)
            Spacer()
            HStack {
                Spacer()
                Text(token.stats)
                    .fontWeight(.semibold)
            }
        }
        .foregroundColor(model.textColor(for: token))
        .shadow(color: model.shadowColor(for: token), radius: 1)
        .padding(8)
        .background(model.background(token: token))
        .frame(width: 100, height: 150, alignment: .center)
        .padding(4)
    }
}

struct TokenView_Previews: PreviewProvider {
    static var previews: some View {
        TokenView(token: Token.list[0])
    }
}

3      

This post just solved my issue about the ForEach that works better with constants and do not update the array when removing items...

Thanks :)

3      

A crash when deleting the last item is a known bug in MacOS 10.15 (Catalina) and iOS 13. I don’t know about later versions of MacOS or iOS 14. See:

https://blog.apptekstudios.com/2020/05/quick-tip-avoid-crash-when-using-foreach-bindings-in-swiftui/

https://stackoverflow.com/questions/63079221/deleting-list-elements-from-swiftuis-list

3      

Hacking with Swift is sponsored by Essential Developer

SPONSORED Join a FREE crash course for mid/senior iOS devs who want to achieve an expert level of technical and practical skills – it’s the fast track to being a complete senior developer! Hurry up because it'll be available only until April 28th.

Click to save your free spot now

Sponsor Hacking with Swift and reach the world's largest Swift community!

Archived topic

This topic has been closed due to inactivity, so you can't reply. Please create a new topic if you need to.

All interactions here are governed by our code of conduct.

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.