Swiftui List .Ondelete: Index Out of Range

SwiftUI List .onDelete: Index out of range

This is a SwiftUI bug reported in Deleting list elements from SwiftUI's List.

The solution is to use the extension from here that prevents accessing invalid bindings:

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)
}
}
List {
ForEach(vm.steps.indices, id: \.self) { index in
Safe(self.$vm.steps, index: index) { binding in
TheSlider(value: binding.theValue, index: vm.steps[index].theIndex)
}
}
.onDelete(perform: { indexSet in
self.vm.removeStep(index: indexSet)
})
}

SwiftUI .onDelete throws Fatal Error: Index out of range

Found a solution. With respect to the this answer to a related question, it came clear. Apperantly, with using the .indices to fullfill ForEach makes onDelete not to work. Instead, going through directly Elements of an array and creating proxy Bindings is working.

To be it more clear here are the ContentView, DetailView and an extension for Binding to avoid cluttering view.

ContentView

struct ContentView: View {

@EnvironmentObject var store: DataStore

var body: some View {
NavigationView {
List {
ForEach(self.store.data, id: \.id /*with or without id*/) { (data) in
NavigationLink(destination: DetailView(data: /*created custom initializer*/ Binding(from: data))) {
Text(data.name)
}
}.onDelete { (set) in
self.store.data.remove(atOffsets: set)
}
}.navigationBarTitle("List")
}
}
}

DetailView

struct DetailView: View {

@Binding var data: MyData

var body: some View {
List {
TextField("name", text: self.$data.name)
}
.navigationBarTitle(self.data.name)
.listStyle(GroupedListStyle())
}
}

Binding extension

extension Binding where Value: MyData {
init(from data: MyData) {
self.init(get: {
data as! Value
}) {
dataStore.data[dataStore.data.firstIndex(of: data)!] = $0
}
}
}

This way, both onDelete and publishing changes by directly updating the object inside detail view will work.

Index Out Of Range When Using .onDelete -SwiftUI

I was able to get this to work by modifying my textfield to work the following way:

EditorView(container: self.$recipeStep, index: index, text: recipeStep[index])

The solution was found here, posted by Asperi: https://stackoverflow.com/a/58911168/12299030

I had to modify his solution a bit to fit my forEach, but overall it works perfectly!

Getting Index out of range when using onDelete

Note: This is no longer an issue in iOS15 since List now support binding

This certainly seems to be a bug within SwiftUI itself.
After research, It found that the problem comes from the TextField binding. If you replace the TextField with a simple Text View, everything will work correctly. It looks like after deleting an item, the TextField binding is trying to access the deleted item, and it can not find it which causes a crash.
This article helped me tackle this problem, Check out SwiftbySundell

So to fix the problem, we’re going to have to dive a bit deeper into Swift’s collection APIs in order to make our array indices truly unique.

  1. Introduce a custom collection that’ll combine the indices of another collection with the identifiers of the elements that it contains.
struct IdentifiableIndices<Base: RandomAccessCollection>
where Base.Element: Identifiable {

typealias Index = Base.Index

struct Element: Identifiable {
let id: Base.Element.ID
let rawValue: Index
}

fileprivate var base: Base
}

  1. Make our new collection conform to the standard library’s RandomAccessCollection protocol,
extension IdentifiableIndices: RandomAccessCollection {
var startIndex: Index { base.startIndex }
var endIndex: Index { base.endIndex }

subscript(position: Index) -> Element {
Element(id: base[position].id, rawValue: position)
}

func index(before index: Index) -> Index {
base.index(before: index)
}

func index(after index: Index) -> Index {
base.index(after: index)
}
}

  1. Make it easy to create an IdentifiableIndices instance by adding the following computed property to all compatible base collections (that is, ones that support random access, and also contains Identifiable elements):
extension RandomAccessCollection where Element: Identifiable {
var identifiableIndices: IdentifiableIndices<Self> {
IdentifiableIndices(base: self)
}
}

  1. Finally, let’s also extend SwiftUI’s ForEach type with a convenience API that’ll let us iterate over an IdentifiableIndices collection without also having to manually access the rawValue of each index:
extension ForEach where ID == Data.Element.ID,
Data.Element: Identifiable,
Content: View {
init<T>(
_ data: Binding<T>,
@ViewBuilder content: @escaping (T.Index, Binding<T.Element>) -> Content
) where Data == IdentifiableIndices<T>, T: MutableCollection {
self.init(data.wrappedValue.identifiableIndices) { index in
content(
index.rawValue,
Binding(
get: { data.wrappedValue[index.rawValue] },
set: { data.wrappedValue[index.rawValue] = $0 }
)
)
}
}
}

  1. Finally, in your ContentView, you can change the ForEach into:
ForEach($items) { index, item in
TextField("", text: item.name, onCommit: {
saveItems()
})
}

The @Binding property wrapper lets us declare that one value actually comes from elsewhere, and should be shared in both places. When deleting the Item in our list, the array items changes rapidly which in my experience causes the issue.
It seems like SwiftUI applies some form of caching to the collection bindings that it creates, which can cause an outdated index to be used when subscripting into our underlying Item array — which causes the app to crash with an out-of-bounds error.

SwiftUI .onDelete wrong index when list is filtered

ingredientsStore.items.remove(atOffsets: offsets)

here offsets is index of searchResults.

so you should get Ingredient items from searchResults, then delete it from ingredientsStore.items。

try this:

for offset in offsets{
if let index = ingredientsStore.items.firstIndex(searchResults[offset]) {
ingredientsStore.items.remove(at: index)
}
}


Related Topics



Leave a reply



Submit