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 Asset
s 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
Detect Left and Right Click Events on Nsstatusitem (Swift)
Cannot Add Alamofire to Swift Project
Why Is It Legal to Mutate an Actor's Nonsendable Property
Binding in a Foreach in Swiftui
How to Get Coreml in Pure Playground Files
Why Swift Throws Error When Using Optional Param in Closure Func
Output Compile Durations for Swift Files
How to Make a Swift Enum with Associated Values Equatable
Add a Border with Cornerradius to an Image in Swiftui Xcode Beta 5
Save Arfacegeometry to Obj File
Swift 3 - Search Result Also with Diacritics
Realitykit as a Framework to Build 3D Nonar Apps
In Swift, Does Int Have a Hidden Initializer That Takes a String
How to Draw a Line Between Two Points in Scenekit
How to Cast an Any Value with Nil in It to a Any
Add Environmentobject in Swiftui 2.0
Difference Between Type Method and Type Instance Method, etc