Bart van Kuik
Bart van Kuik

Reputation: 4862

Deleting row in Form results in Fatal error: Index out of range

I have the following code, which will compile only in Xcode 13 beta, not in Xcode 12. The project is targeting iOS 13 and up. Note that it uses the new syntax that can pass a binding to an array element, which is what I need in my project (but this demo code could do without).

class MyViewModel: ObservableObject {
    @Published var model: MyModel
    
    init() {
        self.model = MyModel(components: ["alpha", "bravo", "charlie", "delta", "echo", "foxtrot"])
    }
}

struct MyModel {
    var components: [String]
}

struct ContentView: View {
    @EnvironmentObject var viewModel: MyViewModel

    var body: some View {
        NavigationView {
            Form {
                Section(header: Text("Components")) {
                    ForEach(self.$viewModel.model.components, id: \.self) { $component in
                        Text(component)
                    }
                    .onDelete(perform: delete)
                }
            }
        }.navigationViewStyle(StackNavigationViewStyle())
    }

    private func delete(_ indices: IndexSet) {
        for index in indices {
            self.viewModel.model.components.remove(at: index)
        }
    }
}

Furthermore, the ContentView() must be injected with the correct environment object, i.e.:

struct FormDeleteCrashDemo_iOS13App: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
                .environmentObject(MyViewModel())
        }
    }
}

When running this code on iPad or iPhone running iOS 14, the user can swipe-left-to-delete. Deleting the bottom row is fine. Deleting any other row will crash with the following error:

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

When I replace the Form with List, then the crash does not occur. Why?

Upvotes: 0

Views: 160

Answers (1)

Phil Dukhov
Phil Dukhov

Reputation: 88024

This looks like a bug to me. Crash happens exactly with Form(works fine with List)

I've reported it it to feedback assistant, so let's hope it'll be fixed before release.

Meanwhile you can try this CustomForEach, inspired by my ListIndexed. All animations looks totally fine.

struct CustomForEach<Data: MutableCollection&RandomAccessCollection, RowContent: View, ID: Hashable>: View, DynamicViewContent where Data.Index : Hashable
{
    let forEach: ForEach<[(Data.Index, Data.Element)], ID, RowContent>
    
    init(_ data: Binding<Data>,
         @ViewBuilder rowContent: @escaping (Binding<Data.Element>) -> RowContent
    ) where Data.Element: Identifiable, Data.Element.ID == ID {
        forEach = ForEach(
            Array(zip(data.wrappedValue.indices, data.wrappedValue)),
            id: \.1.id
        ) { i, _ in
            rowContent(Binding(get: { data.wrappedValue[i] }, set: { data.wrappedValue[i] = $0 }))
        }
    }
    
    init(_ data: Binding<Data>,
         id: KeyPath<Data.Element, ID>,
         @ViewBuilder rowContent: @escaping (Binding<Data.Element>) -> RowContent
    ) {
        forEach = ForEach(
            Array(zip(data.wrappedValue.indices, data.wrappedValue)),
            id: (\.1 as KeyPath<(Data.Index, Data.Element), Data.Element>).appending(path: id)
        ) { i, _ in
            rowContent(Binding(get: { data.wrappedValue[i] }, set: { data.wrappedValue[i] = $0 }))
        }
    }
    
    init(_ data: Binding<Data>,
         @ViewBuilder rowContent: @escaping (Binding<Data.Element>) -> RowContent
    ) where ID == Data.Element {
        forEach = ForEach(
            Array(zip(data.wrappedValue.indices, data.wrappedValue)),
            id: \.1
        ) { i, _ in
            rowContent(Binding(get: { data.wrappedValue[i] }, set: { data.wrappedValue[i] = $0 }))
        }
    }
    
    var body: some View {
        forEach
    }
    
    var data: [(Data.Index, Data.Element)] {
        forEach.data
    }
}

Upvotes: 1

Related Questions