When I'm trying to delete an item in list I'm getting the following error:
Swift/ContiguousArrayBuffer.swift:580: Fatal error: Index out of range 2021-05-07 09:59:38.171277+0200 Section[4462:220358] Swift/ContiguousArrayBuffer.swift:580: Fatal error: Index out of range
I have been searching for a solution but none of the once I'v found seems to work on my code.
FIY this is my first app in SwiftUI or Swift for that matter so I'm totally new to this.
So if someone could help me out and explain what I need to change and why I would be so grateful :)
Here is some code I hope will help get to the bottom of the problem.
My model:
//
// Item.swift
// Section
//
// Created by Niklas Peterson on 2021-03-11.
//
import Foundation
struct Item: Identifiable, Hashable, Codable {
var id = UUID().uuidString
var name: String
}
extension Item {
static func getAll() -> [Item] {
let key = UserDefaults.Keys.items.rawValue
guard let items: [Item] = UserDefaults.appGroup.getArray(forKey: key) else {
let items: [Item] = [.section1]
UserDefaults.appGroup.setArray(items, forKey: key)
return items
}
return items
}
static let section1: Item = {
return Item(name: "Section")
}()
}
extension Item {
static func fromId(_ id: String) -> Item? {
getAll().first { $0.id == id }
}
}
UserDefaults:
//
// UserDefaults+Ext.swift
// Section
//
// Created by Niklas Peterson on 2021-03-11.
//
import Foundation
extension UserDefaults {
static let appGroup = UserDefaults(suiteName: "************ hidden ;) ")!
}
extension UserDefaults {
enum Keys: String {
case items
}
}
extension UserDefaults {
func setArray<Element>(_ array: [Element], forKey key: String) where Element: Encodable {
let data = try? JSONEncoder().encode(array)
set(data, forKey: key)
}
func getArray<Element>(forKey key: String) -> [Element]? where Element: Decodable {
guard let data = data(forKey: key) else { return nil }
return try? JSONDecoder().decode([Element].self, from: data)
}
}
ContentView:
//
// ContentView.swift
// Section
//
// Created by Niklas Peterson on 2021-03-11.
//
import SwiftUI
struct ContentView: View {
@State private var items = Item.getAll()
func saveItems() {
let key = UserDefaults.Keys.items.rawValue
UserDefaults.appGroup.setArray(items, forKey: key)
}
func move(from source: IndexSet, to destination: Int) {
items.move(fromOffsets: source, toOffset: destination)
saveItems()
}
func delete(at offsets: IndexSet) {
items.remove(atOffsets: offsets)
saveItems()
}
var body: some View {
NavigationView {
List {
ForEach(items.indices, id: \.self) { index in
TextField("", text: $items[index].name, onCommit: {
saveItems()
})
}
.onDelete(perform: delete)
.onMove(perform: move)
}
.toolbar {
ToolbarItemGroup(placement: .primaryAction) {
HStack {
Button(action: {
self.items.insert(Item(name: ""), at: 0)
}) {
Image(systemName: "plus.circle.fill")
}
EditButton()
}
}
}
.navigationBarTitle("Sections")
.listStyle(InsetGroupedListStyle())
}
}
}
iOS15
since List
now supports binding
This certainly seems to be a bug within SwiftUI
itself.
After research, It found that the problem comes from the TextField
binding. If you replace the TextField
with a simple Text
View, everything will work correctly. It looks like after deleting an item, the TextField
binding is trying to access the deleted
item, and it can not find it which causes a crash.
This article helped me tackle this problem, Check out SwiftbySundell
So to fix the problem, we’re going to have to dive a bit deeper into Swift’s collection APIs in order to make our array indices truly unique.
struct IdentifiableIndices<Base: RandomAccessCollection>
where Base.Element: Identifiable {
typealias Index = Base.Index
struct Element: Identifiable {
let id: Base.Element.ID
let rawValue: Index
}
fileprivate var base: Base
}
RandomAccessCollection
protocol,extension IdentifiableIndices: RandomAccessCollection {
var startIndex: Index { base.startIndex }
var endIndex: Index { base.endIndex }
subscript(position: Index) -> Element {
Element(id: base[position].id, rawValue: position)
}
func index(before index: Index) -> Index {
base.index(before: index)
}
func index(after index: Index) -> Index {
base.index(after: index)
}
}
IdentifiableIndices
instance by adding the following computed property to all compatible base collections (that is, ones that support random access, and also contains Identifiable
elements):extension RandomAccessCollection where Element: Identifiable {
var identifiableIndices: IdentifiableIndices<Self> {
IdentifiableIndices(base: self)
}
}
ForEach
type with a convenience API that’ll let us iterate over an IdentifiableIndices
collection without also having to manually access the rawValue
of each index:extension ForEach where ID == Data.Element.ID,
Data.Element: Identifiable,
Content: View {
init<T>(
_ data: Binding<T>,
@ViewBuilder content: @escaping (T.Index, Binding<T.Element>) -> Content
) where Data == IdentifiableIndices<T>, T: MutableCollection {
self.init(data.wrappedValue.identifiableIndices) { index in
content(
index.rawValue,
Binding(
get: { data.wrappedValue[index.rawValue] },
set: { data.wrappedValue[index.rawValue] = $0 }
)
)
}
}
}
ContentView
, you can change the ForEach
into:ForEach($items) { index, item in
TextField("", text: item.name, onCommit: {
saveItems()
})
}
The @Binding
property wrapper lets us declare that one value actually comes from elsewhere, and should be shared in both places. When deleting the Item
in our list, the array items
changes rapidly which in my experience causes the issue.
It seems like SwiftUI
applies some form of caching to the collection bindings that it creates, which can cause an outdated index to be used when subscripting into our underlying Item
array — which causes the app to crash with an out-of-bounds error.