Fatal error: Index out of range: file Swift/ContiguousArrayBuffer.swift when removing element from array

Viewed 10586

I have a form to select some users and assign them a int value.

The model:

class ReadingTime: Identifiable, Hashable {
    var id: Int
    @State var user: User
    @Published var value: Int

    func hash(into hasher: inout Hasher) {
        hasher.combine(id)
    }
    
    static func == (lhs: ReadingTime, rhs: ReadingTime) -> Bool {
        lhs.id == rhs.id
    }
    
    init(id: Int, user: User, value: Int) {
        self.id = id
        self._user = State(wrappedValue: user)
        self.value = value
    }
}

The view:

@Binding var times: [ReadingTime]
@State var newUser: User?

func didSelect(_ user: User?) {
    if let user = user {
        readingTime.append(ReadingTime(id: readingTime.nextMaxId,
                                       user: user,
                                       value: 0))
    }
}

// In the body:
VStack(alignment: .leading, spacing: 0) {
    HStack {
        Picker("Select a user", selection: $newUser.onChange(didSelect)) {
                    ForEach(users) {
                        Text($0.name).tag(Optional($0))
                    }
                }
                .id(users)
            }
            VStack(spacing: 8) {
                ForEach(0..<times.count, id: \.self) { i in
                    HStack(spacing: 0) {                            
                        Text(times[i].user.name)
                        TextField("ms", value: $times[i].value, formatter: NumberFormatter())
                        Button(action: {
                            NSApp.keyWindow?.makeFirstResponder(nil)
                            if let index = times.firstIndex(where: { $0 == times[i] }) {
                                times.remove(at: index)
                            }
                            newUser = nil
                        }, label: {
                            Text("REMOVE")
                        })
                    }
                }
            }
        }
    }
}

It looks like this:

enter image description here

However, when deleting an entry in the list, I get this error:

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

What's going on here?

2 Answers

Modifying the number of items of an array while being enumerated is an evil trap.

0..<times.count creates a temporary static range.

If you remove the first item in the array index 1 becomes 0, index 2 becomes 1 and so on.

Unfortunately there is no index times.count-1 anymore and you get the Index out of range crash when the loop reaches the last index.

You can avoid the crash if you enumerate the array reversed.

Since there is no accepted answer:

Don't use the index of your array to get the specific object inside your ForEach. Like you did before, you're able to access your objects without using count. Using count causes your Index out of range error.

Do it like this:

ForEach(times) { time in
   // ...
   Text(time.user.name)
   // ...
}
Related