Peter Warbo
Peter Warbo

Reputation: 11710

Crash when array is empty in iOS 14.3

I'm trying to show some placeholder data when the array is empty. This works in iOS 13.7 but something has changed in iOS 14.3 so when the last item is deleted you get this crash:

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

If I comment out testStore.data.isEmpty and just return the Form I get no crash.

How can I show placeholder when array is empty in iOS 14.3?

struct Test: Identifiable {
    var text: String
    var id: String { text }
}

extension Test {
    final class Store: ObservableObject {
        @Published var data = [Test(text: "Hi"), Test(text: "Bye")]
    }
}

struct TestList: View {
    
    @EnvironmentObject var testStore: Test.Store
    
    var body: some View {
        Group {
            if testStore.data.isEmpty {
                Text("Empty")
            } else {
                Form {
                    ForEach(testStore.data.indices, id: \.self) { index in
                        TestRow(test: $testStore.data[index], deleteHandler: { testStore.data.remove(at: index) })
                    }
                }
            }
        }
    }
}

struct TestRow: View {
    
    @Binding var test: Test
    let deleteHandler: (() -> ())
    
    var body: some View {
        HStack {
            Text(test.text)
                .font(.headline)
            Spacer()
            Button(action: deleteHandler, label: Image(systemName: "trash"))
        }
    }
}

Upvotes: 0

Views: 434

Answers (1)

pawello2222
pawello2222

Reputation: 54516

You can use the extension proposed here:

struct Safe<T: RandomAccessCollection & MutableCollection, C: View>: View {
    typealias BoundElement = Binding<T.Element>
    private let binding: BoundElement
    private let content: (BoundElement) -> C

    init(_ binding: Binding<T>, index: T.Index, @ViewBuilder content: @escaping (BoundElement) -> C) {
        self.content = content
        self.binding = .init(get: { binding.wrappedValue[index] },
                             set: { binding.wrappedValue[index] = $0 })
    }

    var body: some View {
        content(binding)
    }
}

Then, if you also want to keep ForEach instead of List you can do:

struct TestList: View {
    @EnvironmentObject var testStore: Test.Store

    var body: some View {
        Group {
            if testStore.data.isEmpty {
                Text("Empty")
            } else {
                Form {
                    ForEach(testStore.data.indices, id: \.self) { index in
                        Safe($testStore.data, index: index) { binding in
                            TestRow(test: binding, deleteHandler: { testStore.data.remove(at: index) })
                        }
                    }
                }
            }
        }
    }
}

Upvotes: 1

Related Questions