I have an array of elements that contains a nested array of other elements inside. When deleting a row of an array, sometimes a crash occurs with the message
'Swift/ContiguousArrayBuffer.swift:600: Fatal error: Index out of range'
not pointing at concrete line of code.
Here's my minimal reproducible code:
// View components
struct ContentView: View {
@StateObject var viewModel: ViewModel = .init()
var body: some View {
ScrollView {
LazyVStack {
ForEach($viewModel.assetsRows, id: \.self) { assetsRow in
VStack {
Button(action: {
viewModel.deleteSelected(assetsIn: assetsRow.wrappedValue)
}, label: {
HStack {
Image(systemName: "trash")
Text("Delete row")
}
})
RowView(assetsRow: assetsRow)
}
}
}
}
}
}
struct RowView: View {
@Binding var assetsRow: AssetsRowModel
var body: some View {
ScrollView(.horizontal) {
LazyHStack {
ForEach($assetsRow.items, id: \.self) { item in
GridItemView(
assetItem: item,
image: .init(systemName: "photo.fill")
)
}
}
}
}
}
struct GridItemView: View {
@Binding var assetItem: AssetItem
@State var image: Image?
var body: some View {
Group {
if let image = image {
image
} else {
ProgressView()
}
}
.frame(width: 200, height: 120)
.overlay(alignment: .bottomTrailing) {
Toggle(isOn: $assetItem.isSelected) {
Text("checkmark")
}
.padding(4)
}
.onAppear {
// fetch image logic
}
}
}
@MainActor final class ViewModel: ObservableObject {
@Published var assetsRows: [AssetsRowModel] = {
var array: [AssetsRowModel] = []
for i in 0..<30 {
array.append(.init(items: [.init(), .init(), .init()]))
}
return array
}()
// removing items causes crash (not 100% times)
func deleteSelected(assetsIn row: AssetsRowModel) {
withAnimation {
assetsRows.removeAll { element in
element.id == row.id
}
}
}
// other fetching logic
}
// Models
struct AssetsRowModel: Identifiable, Equatable, Hashable {
var id = UUID()
var items: [AssetItem]
}
struct AssetItem: Identifiable, Hashable {
var id = UUID()
var isSelected = false
}
extension AssetItem: Equatable {
static func ==(lhs: AssetItem, rhs: AssetItem) -> Bool {
(lhs.id == rhs.id)
}
}
Tried to change @Binding to @State in RowView, it's prevent the crash, but isSelected doesn't working properly, because it's not 'binding' with viewModel's value.
I guess this is an internal SwiftUI bug. (Xcode 15.4, iOS 17+)
@Stateand@StateObjectproperties should always (without exception) be private. This means that the way you create theGridItemViewis already problematic. States should never be injected from outside, but always generated in the view itself. Bindings are there for external injections.