Swiftui Foreach Index Out of Range Error When Removing Row

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: 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 Index out of range in ForEach

As per Answer at stackoverflow link

Create a struct as under

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 wrap your code for accessing it as under

Safe(self.$palettesOO.palettes, index: idx) { binding in
//Text(binding.wrappedValue.palName)
TextField("ASD", text: binding.palName)
//TextField("ASD", text: $palettesOO.palettes[palettesOO.palettes.count - 1].palName)
.frame(width: 100, height: 100).background(Color.red)
.contextMenu(ContextMenu(menuItems: {
Button(action: {}, label: {
Text("Rename")
})
Button(action: { Manager.RemovePalette(name: binding.wrappedValue.palName); print("Len \(palettesOO.palettes.count)") }, label: {
Text("Delete")
})
}))
}

I hope this can help you ( till it is corrected in Swift )

Got Fatal error: Index out of range : show index in list item for swiftui

@State is property wrapper, which will force the View in which it is defined to recalculate its body.

In your case, if you remove the item at index,

HStack {
Text("\(index)")
Spacer()
Text("\(self.list[index].name)")
}
.background(Color.gray.opacity(0.001))
.onTapGesture {
self.list.remove(at: index)
}

the Text inside HStack

Text("\(self.list[index].name)")

crash, just because list[index] doesn't exist any more.

Using

ForEach(list.indices, id:\.self) { index in ... }

instead of

ForEach(list.indices) { index in ... }

will force SwiftUI to recreate TestView (see the id:\.self in ForEach constructor)

SwiftUI will make fresh copy of TestView while using fresh value of property wrapped in @State property wrapper.

UPDATE

Please, don't update your question ...

Your last code version 4 is total mess, so I rewrote it to something you able to copy - paste - run

import SwiftUI

struct Name: Identifiable, Hashable {
var id: String = UUID().uuidString
var name: String
var marked: Bool
init(_ name: String, marked: Bool = false) { self.name = name; self.marked = marked }
}

struct ContentView: View {
@State private var list: [Name] = [Name("test1"), Name("test2"), Name("test3", marked: true), Name("test4"), Name("test5", marked: true), Name("test6"), Name("test7"), Name("test8")]
@State private var showMarkedOnly = false

var body: some View {
VStack{
Toggle(isOn: $showMarkedOnly) {
Text("show marked only")
}.padding(.horizontal)
List {
ForEach(Array(zip(0..., list)).filter({!self.showMarkedOnly || $0.1.marked}), id: \.1.id) { index, name in
HStack {
Text("\(index)").foregroundColor(name.marked ? .red : .gray)
Spacer()
Text("\(name.name)")
}
.background(Color.gray.opacity(0.001))
.onTapGesture {
self.list.remove(at: index)
}
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

it should looks like Sample Image

UPDATE based on discussion

ForEach different versions of constructors use internally different functionality of ViewBuilder

@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
extension ViewBuilder {

/// Provides support for "if" statements in multi-statement closures, producing an `Optional` view
/// that is visible only when the `if` condition evaluates `true`.
public static func buildIf<Content>(_ content: Content?) -> Content? where Content : View

/// Provides support for "if" statements in multi-statement closures, producing
/// ConditionalContent for the "then" branch.
public static func buildEither<TrueContent, FalseContent>(first: TrueContent) -> _ConditionalContent<TrueContent, FalseContent> where TrueContent : View, FalseContent : View

/// Provides support for "if-else" statements in multi-statement closures, producing
/// ConditionalContent for the "else" branch.
public static func buildEither<TrueContent, FalseContent>(second: FalseContent) -> _ConditionalContent<TrueContent, FalseContent> where TrueContent : View, FalseContent : View
}

This is about "implementation details" and hopefully it will be documented in next release. SwiftUI is still in very early stage of development, we have to be careful.

Lets try to force SwiftUI to follow our own way!
First separate RowView

struct RowView: View {
var showMarkedOnly: Bool
var index: Int
var name: Name
//@ViewBuilder
var body: some View {
if !self.showMarkedOnly || name.marked {
HStack {
Text(verbatim: index.description).foregroundColor(name.marked ? .red : .gray)
Spacer()
Text(verbatim: name.name)
}
.background(Color.gray.opacity(0.001))

}
}
}

Compiler complains with

Function declares an opaque return type, but has no return statements in its body from which to infer an underlying type

Uncomment the line to wrap our body

struct RowView: View {
var showMarkedOnly: Bool
var index: Int
var name: Name
@ViewBuilder
var body: some View {
if !self.showMarkedOnly || name.marked {
HStack {
Text(verbatim: index.description).foregroundColor(name.marked ? .red : .gray)
Spacer()
Text(verbatim: name.name)
}
.background(Color.gray.opacity(0.001))

}
}
}

Now we can use the code the way you like :-)

struct ContentView: View {
@State private var list: [Name] = [Name("test1"), Name("test2"), Name("test3", marked: true), Name("test4"), Name("test5", marked: true), Name("test6"), Name("test7"), Name("test8")]
@State private var showMarkedOnly = false

var body: some View {
VStack{
Toggle(isOn: $showMarkedOnly) {
Text("show marked only")
}.padding(.horizontal)
List {
ForEach(Array(zip(0..., list)), id: \.1.id) { (index, name) in
RowView(showMarkedOnly: self.showMarkedOnly, index: index, name: name).onTapGesture {
self.list.remove(at: index)
}
}
}
}
}
}

The final result uses now buildIf<Content> construct and all code works again (the result looks exactly the same as shown above)

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.



Related Topics



Leave a reply



Submit