1

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())
        } 
    }
    
}

davidev
  • 7,694
  • 5
  • 21
  • 56
Niklas
  • 35
  • 8
  • @asperi I have read your answers on other questions on this matter before, but I can't seem to get it to work. And don't totally understand what I need to do to make the changes fit my case. I just tried this: https://stackoverflow.com/questions/66712572/swiftui-list-ondelete-index-out-of-range but didn't that to work either I normally only do design, but wanting to learn how to code in SwiftUI :) – Niklas May 07 '21 at 10:53

1 Answers1

5

Note: This is no longer an issue in 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.

  1. Introduce a custom collection that’ll combine the indices of another collection with the identifiers of the elements that it contains.
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
}
  1. Make our new collection conform to the standard library’s 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)
    }
}
  1. Make it easy to create an 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)
    }
}
  1. Finally, let’s also extend SwiftUI’s 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 }
)
            )
        }
    }
}
  1. Finally, in your 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.

cedricbahirwe
  • 1,274
  • 4
  • 15
  • 1
    I just tested it and it works! I had no idea that it was the TextField causing the crash. I have been searching for days and trying all the solutions I have found here on StackOverflow. Thought it was related to the bug with OnDelete on List or ForEach which many ppl have been mentioning. I'll read threw everything you wrote some more times to try to understand it. And also check the link you posted! You made my Friday som much better! Thanks you so much! – Niklas May 07 '21 at 14:42