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 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 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
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
iOS 7.0 and Arc: Uitableview Never Deallocated After Rows Animation
Taking Screenshots in the Background (Ios) - Improving Performance
How to Detect the Scroll Direction from the Uicollectionview
Game Engine Collison Bitmask... Why 0X01 etc
Taking Photo with Custom Camera Swift 3
Removing iPad Support from App
Uiapplicationlaunchoptionsremotenotificationkey Not Getting Userinfo
Show Data in Uipickerview Second Component Based on First Component Selection
Drawing Rounded Rect in Core Graphics
Code Signing Issue in Xcode Version 8
Allow "Auto Lock" While Video Is Being Played
Gcd - Main VS Background Thread for Updating a Uiimageview
How to Properly Send an Image to Cloudkit as Ckasset
Swift- Custom Uitableviewcell Delegate to Uiviewcontroller Only One Protocol Works