I try to lay out my UI elements in rows - two elements in a row, for that I use two ForEach, one for rows and one for elements in a row. Each UI element has @Binding so I pass a structure from my model's array. I should be able to add or remove the elements dynamically and everything works except one thing - when I remove the only element from a row my app crashes with error Fatal error: Index out of range: file Swift/ContiguousArrayBuffer.swift, line 444. I have read several topics on SO but I haven't found an answer.
This is how my code looks like:
struct PickerElement: Hashable {
let id: Int
let title: String
let value: Int
}
struct CellModel: Hashable {
var element: PickerElement?
var error: String?
}
struct PickerButton: View {
@Binding var value: CellModel?
@Binding var error: String?
(...)
}
class MyModel: ObservableObject {
@Published var counter = 0
@Published var cellModels = [CellModel]()
private var cancellables = Set<AnyCancellable>()
$counter.sink { value in
let diff = value - self.cellModels.count
if diff > 0 {
self.cellModels.append(contentsOf:
Array(repeating: CellModel(), count: diff)
)
} else if diff < 0 {
self.cellModels = Array(
self.cellModels.prefix(value)
)
}
}.store(in: &cancellables)
}
struct MyView: View {
@ObservedObject var model: MyModel
var body: some View {
VStack(spacing: 8) {
layOutElements()
}
}
@ViewBuilder
func layOutElements() -> some View {
let elementsCount = model.cellModels.count
if elementsCount > 0 {
VStack {
HStack {
Spacer()
Text("Some title").font(.caption)
Spacer()
}.padding()
// count number of rows
let rowsCount = Int(ceil(Double(elementsCount) / 2.0))
// lay out rows
ForEach(0 ..< rowsCount, id: \.self) { rowIndex in
layOutRow(rowIndex: rowIndex,
elementsCount: elementsCount,
rowsCount: rowsCount)
}
}
}
}
@ViewBuilder
private func layOutRow(rowIndex: Int, elementsCount: Int, rowsCount: Int) -> some View {
HStack(alignment: .top, spacing: 8) {
let firstCellInRowIndex = rowIndex * 2
let lastCellInRowIndex = min(elementsCount - 1, firstCellInRowIndex + 1)
ForEach(firstCellInRowIndex ... lastCellInRowIndex, id: \.self) { elementIndex in
PickerButton(value: $model.cellModels[elementIndex].element, // <--- *1
error: $model.cellModels[elementIndex].error) // <--- *2
}
// *1 , *2 - if I changed the lines and pass dummy bindings (not array's elements) there, the code would work without any glitches
if rowIndex == rowsCount - 1 && !elementsCount.isMultiple(of: 2) {
Spacer()
.frame(minWidth: 0, maxWidth: .infinity)
}
}
}
If I change a value of @Published var counter = 0 everything works properly, views are added and removed, but while decrementing if SwiftUI tries to remove the last remaining element from a row the app crashes. As I have commented in the code above, if I don't bind PickerButton to the structures from my model's array, the app doesn't crash. How to fix this issue? (I need to use indexes because I have to count rows and cells in a row)