9

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?

Jonathan Hall
  • 75,165
  • 16
  • 143
  • 189
Another Dude
  • 1,204
  • 4
  • 15
  • 33
  • 1
    Does this answer your question https://stackoverflow.com/a/61431459/12299030? Also pay attention at https://stackoverflow.com/a/58911168/12299030 description about `ForEach`? – Asperi Nov 19 '20 at 11:39
  • @Asperi Thank you, I followed what you described in the 2nd link and it works perfectly fine – Another Dude Nov 19 '20 at 15:32

2 Answers2

7

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.

vadian
  • 274,689
  • 30
  • 353
  • 361
  • Thank you for your answer. I perfectly understand the issue, but can you explain how to "enumerate the array reversed"? – Another Dude Nov 19 '20 at 14:44
  • Instead of starting at index 0 start at the last index and count backwards. – vadian Nov 19 '20 at 15:04
  • 1
    @vadian in 2023 also facing this issue. I think using `reversed()` worked for me if I delete the last element from the array. But If I delete the first element, crash happens. – mmk Jun 29 '23 at 01:54
0

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)
   // ...
}
Tom Hakemann
  • 262
  • 3
  • 9