Swiftui Out of Index When Deleting an Array Element in Foreach

SwiftUI out of index when deleting an array element in ForEach

Here is possible solution - make body of SecondView undependable of ObservableObject.

Tested with Xcode 11.4 / iOS 13.4 - no crash

struct SecondView: View {

@ObservedObject var elementHolder: ElementHolder
var index: Int
let value: String

init(elementHolder: ElementHolder, index: Int) {
self.elementHolder = elementHolder
self.index = index
self.value = elementHolder.elements[index]
}

var body: some View {
HStack {
Text(value) // not refreshed on delete
Button(action: {
self.elementHolder.elements.remove(at: self.index)
}) {
Text("delete")
}
}
}
}

Another possible solution is do not observe ElementHolder in SecondView... for presenting and deleting it is not needed - also no crash

struct SecondView: View {

var elementHolder: ElementHolder // just reference
var index: Int

var body: some View {
HStack {
Text(self.elementHolder.elements[self.index])
Button(action: {
self.elementHolder.elements.remove(at: self.index)
}) {
Text("delete")
}
}
}
}

Update: variant of SecondView for text field (only changed is text field itself)

struct SecondViewA: View {

var elementHolder: ElementHolder
var index: Int

var body: some View {
HStack {
TextField("", text: Binding(get: { self.elementHolder.elements[self.index] },
set: { self.elementHolder.elements[self.index] = $0 } ))
Button(action: {
self.elementHolder.elements.remove(at: self.index)
}) {
Text("delete")
}
}
}
}

SwiftUI: Deleting an item from a ForEach results in Index Out of Range

I think it's because of your ForEach relying on the indices, rather than the Assets themselves. But, if you get rid of the indices, you'll have to write a new binding for the Asset. Here's what I think it could look like:

ForEach(assetStore.assets, id: \.id) { asset in
AssetView(
asset: assetStore.bindingForId(id: asset.id),
assetStore: assetStore,
smallSize: geo.size.height <= 667
)
.padding(.bottom)
}

And then in your AssetStore:

func bindingForId(id: UUID) -> Binding<Asset> {
Binding<Asset> { () -> Asset in
self.assets.first(where: { $0.id == id }) ?? Asset()
} set: { (newValue) in
self.assets = self.assets.map { asset in
if asset.id == id {
return newValue
} else {
return asset
}
}
}
}

SwiftUI - ForEach deletion transition always applied to last item only

The reason you're seeing this behavior is because you use an index as an id for ForEach. So, when an element is removed from the cards array, the only difference that ForEach sees is that the last index is gone.

You need to make sure that the id uniquely identifies each element of ForEach.

If you must use indices and have each element identified, you can either use the enumerated method or zip the array and its indices together. I like the latter:

ForEach(Array(zip(cards.indices, cards)), id: \.1) { (index, card) in 
//...
}

The above uses the object itself as the ID, which requires conformance to Hashable. If you don't want that, you can use the id property directly:

ForEach(Array(zip(cards.indices, cards)), id: \.1.id) { (index, card) in
//...
}

For completeness, here's the enumerated version (technically, it's not an index, but rather an offset, but for 0-based arrays it's the same):

ForEach(Array(cards.enumerated()), id: \.1) { (index, card) in 
//...
}

SwiftUI: Index out of range when last element gets removed

I found a solution, I changed my forEach to:

ForEach(checklistItems.indices, id: \.self) { index in
HStack {
RowView(
checklistItem:
Binding(
get: { self.checklistItems[index] },
set: { self.checklistItems[index] = $0 }
),
viewModel: viewModel
)
.padding(.leading, 12)

if checklistEditMode {
Button {
checklistItems.remove(at: index)
} label: {
Image("XCircle")
.resizable()
.frame(width: 18, height: 18)
}
.padding(.trailing, 12)
}
}
.padding(.horizontal, 12)
.padding(.top, 12)
.padding(.bottom, 4)

Divider()
.frame(width: 311)
}

and it works now with no crashes, Thanks to this

SwiftUI list ForEach index out of range when last array element deleted

I found the solution to my problem. The problem appears to be that the ForEach is trying to look at the difference between the previous and the new data and is trying to grab deleted core data objects. I fixed the problem by changing the ForEach loop to check if the object we are looking at has been deleted

ForEach(Array(viewModel.rooms.enumerated()), id: \.self.element.id) { index, room in
if room.isFault {
EmptyView()
} else {
RoomRow(room: self.viewModel.rooms[index], viewModel: viewModel, index: index)
}
}

Thread 1: Fatal error: Index out of range when removing from array with @Binding

In your code the ForEach with indicies and id: \.self is a mistake. The ForEach View in SwiftUI isn’t like a traditional for loop. The documentation of ForEach states:

/// It's important that the `id` of a data element doesn't change, unless
/// SwiftUI considers the data element to have been replaced with a new data
/// element that has a new identity.

This means we cannot use indices, enumerated or a new Array in the ForEach. The ForEach must be on the actual array of identifiable items. This is so SwiftUI can track the row Views moving around, which is called structural identity and you can learn about it in Demystify SwiftUI WWDC 2021.

So you have to change your code to something this:

import SwiftUI

struct Item: Identifiable {
let id = UUID()
var num: Int
}

struct IntView: View {

let num: Int

var body: some View {
Text("\(num)")
}
}

struct ArrayView: View {

@State var array: [Item] = [Item(num:0), Item(num:1), Item(num:2)]

var body: some View {
ForEach(array) { item in
IntView(num: item.num)

Button(action: {
if let index = array.firstIndex(where: { $0.id == item.id }) {
array.remoteAt(index)
}
}, label: {
Text("remove")
})

}
}
}

SwiftUI remove array entries within ForEach subviews

For anyone who has the same problem. I've figured out a way on how to make it happen. As the ForEach stores the original array and is not referencing to the original array a possible workaround is to force the ForEach to update itself. You can do it like this:

struct ContentView: View {
@EnvironmentObject var dbhandler: DBhandler
@State var dummyVar : Int = 0

var body: some View {

VStack{
ForEach(dbhandler.arrays.array.indices, id: \.self) {index in
SubView(index: index, dummyVar : dummyVar)
}.id(dummyVar)
}

}
}

and in the SubView you just need to increase the dummyVar by 1.



Related Topics



Leave a reply



Submit