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.
- 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
}
- 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)
}
}
- 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 containsIdentifiable
elements):
extension RandomAccessCollection where Element: Identifiable {
var identifiableIndices: IdentifiableIndices<Self> {
IdentifiableIndices(base: self)
}
}
- Finally, let’s also extend SwiftUI’s
ForEach
type with a convenience API that’ll let us iterate over anIdentifiableIndices
collection without also having to manually access therawValue
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 }
)
)
}
}
}
- Finally, in your
ContentView
, you can change theForEach
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
How to Change/Modify The Displayed Title of an Nspopupbutton
How to Insert UIlabel After UItextfield in UIalertcontroller
Creating Semaphore with Initial Value of 0 Make Issues with Execution
iOS 10. Coredata Insert New Object Sig Abrt
Sprite Kit Game Crashes on Game Over on Tvos 9.1 and iOS 9.2
Is There Any Shorthand Way to Write Cgpoint, Cgrect, etc
Uisearchcontroller Hiding Status Bar on iOS 11 Swift 4
How to Download Datas from Multiple Url in Concurrency Mode
How to Add a Storage Reference in Swift for Firestore
Generating a Simple Algebraic Expression in Swift
Not Getting Screenlock Notification on Swift 4 on Mac
Transparency Issues with Repeated Stamping of Textures on an Mtkview
Metal: Limit Mtlrendercommandencoder Texture Loading to Only Part of Texture
Make Statement More Clear: Checking Object for Key in Dictionary
How to Segue an Image to Another Viewcontroller and Display It Within an Imageview
iOS - Could Not Open Obj File When Convert Mdlasset to Mdlmesh