Swiftui Index Out of Range in Foreach

ForEach - Index out of range?

Let me give you a simple example to show you what happened:

struct ContentView: View {

@State private var lowerBound: Int = 0

var body: some View {

ForEach(lowerBound..<11) { index in
Text(String(describing: index))
}

Button("update") { lowerBound = 5 }.padding()

}

}

if you look at the upper code you would see that I am initializing a ForEach JUST with a Range like this: lowerBound..<11 which it means this 0..<11, when you do this you are telling SwiftUI, hey this is my range and it will not change! It is a constant Range! and SwiftUI says ok! if you are not going update upper or lower bound you can use ForEach without showing or given id! But if you see my code again! I am updating lowerBound of ForEach and with this action I am breaking my agreement about constant Range! So SwiftUI comes and tell us if you are going update my ForEach range in count or any thing then you have to use an id then you can update the given range! And the reason is because if we have 2 same item with same value, SwiftUI would have issue to know which one you say! with using an id we are solving the identification issue for SwiftUI! About id you can use it like this: id:\.self or like this id:\.customID if your struct conform to Hash-able protocol, or in last case you can stop using id if you confrom your struct to identifiable protocol! then ForEach would magically sink itself with that.

Now see the edited code, it will build and run because we solved the issue of identification:

struct ContentView: View {

@State private var lowerBound: Int = 0

var body: some View {

ForEach(lowerBound..<11, id:\.self) { index in
Text(String(describing: index))
}

Button("update") { lowerBound = 5 }.padding()

}

}

SwiftUI - Index out of range

Never retrieve items by hard-coded indices in a ForEach expression.

Do count the array, the loop is skipped (safely) if the array is empty.

ForEach(0..<answers.count) { number in

Or - still simpler – enumerate the items rather than the indices

ForEach(answers, id: \.self) { answer in 
Text("\(answer)")
}

Thread 1: Fatal error: Index out of range In SwiftUI

ForEach with an index-based approach is dangerous in SwiftUI. Instead, make your model identifiable.

class User: ObservableObject, Identifiable {
var id = UUID()
//...

Then, change your loop:

ForEach(appUser.friendAnfrage) { item in
SingleFriendView(user: item)
}

Unrelated to this exact issue, but generally SwiftUI does better with using a struct for a model instead of a class. If a User in friends is updated with your current code, because it's a nested ObservableObject, your View will not get automatically updated.

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: 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 .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.

Using ForEach loop with Binding causes index out of range when array shrinks (SwiftUI)

Finally got the ins and outs of that issue that I was experiencing myself.

The problem is architectural. It is 2 folds:

  1. You are making a copy of your unique source of truth. ForEach loops Textfield but you are passing a copy through Binding. Always work on the single source of truth
  2. Combined with ForEach ... indices is supposed to be a constant range (hence the out of range when you remove an element)

The below code works because it loops through the single source of truth without making a copy and always updates the single source of truth. I even added a method to change the string within the subview since you originally passed it as a binding, I imagine you wanted to change it at some point


import SwiftUI

class DataSource: ObservableObject {
@Published var textArray = ["A","B","C"]
}

struct Test: View {

@EnvironmentObject var data : DataSource

var body:some View {
VStack{
ForEach(self.data.textArray , id: \.self) {text in
TextView(text: self.data.textArray[self.data.textArray.firstIndex(where: {text == $0})!])
.padding()
}

//Array modifying button
Button(action: {
self.data.textArray.removeLast()
}){
Text(" Shrink array ")
.padding()
}
}
}
}

struct TextView:View {

@EnvironmentObject var data : DataSource

var text:String

var body:some View {
VStack {
Text(text)
Button(action: {
let index = self.data.textArray.firstIndex(where: {self.text == $0})!
self.data.textArray[index] = "Z"
}){
Text("Change String ")
.padding()
}
}
}
}

#if DEBUG
struct test_Previews: PreviewProvider {
static var previews: some View {
Test().environmentObject(DataSource())
}
}
#endif

Fatal error: Index out of range in SwiftUI

Instead of getting the number of items in an array and using that index to get items from your array of objects, you can get each item in the foreach instead. In your content view, change your For each to this

ForEach(store.items, id:\.self) { item in
NavigationLink(destination: Detail(item: item)) {
VStack {
Text(item.title)
}
}
}

And Change your detail view to this:

struct Detail: View {
@EnvironmentObject var store: CPStore
@State var item: CPModel

var body: some View {
VStack {
//Error is here
TextField("Recording", text: $item.title)
}
}
}


Related Topics



Leave a reply



Submit