使用 onDelete 时索引超出范围

Nik*_*las 1 swiftui swiftui-list

当我尝试删除列表中的项目时,出现以下错误:

Swift/ContigeousArrayBuffer.swift:580:致命错误:索引超出范围 2021-05-07 09:59:38.171277+0200 部分[4462:220358] Swift/ContigouslyArrayBuffer.swift:580:致命错误:索引超出范围

我一直在寻找解决方案,但我发现的解决方案似乎都不适用于我的代码。

FIY,这是我在 SwiftUI 或 Swift 中的第一个应用程序,所以我对此完全陌生。

因此,如果有人可以帮助我并解释我需要改变什么以及为什么我会非常感激:)

这是一些代码,我希望能帮助解决问题的根源。

我的型号:

//
//  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 }
    }
}

Run Code Online (Sandbox Code Playgroud)

用户默认值:

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

Run Code Online (Sandbox Code Playgroud)

内容查看:

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

Run Code Online (Sandbox Code Playgroud)

ced*_*rwe 5

注意:这不再是问题,iOS15因为List现在支持binding

\n

这看起来肯定是SwiftUI其本身的一个错误。\n经过研究,发现问题出在TextField绑定上。TextField如果用简单的视图替换Text,一切都会正常工作。看起来删除项目后,TextField绑定尝试访问该deleted项目,但找不到它,从而导致崩溃。\n这篇文章帮助我解决了这个问题,请查看SwiftbySundell

\n

因此,为了解决这个问题,我们\xe2\x80\x99必须更深入地研究Swift\xe2\x80\x99s集合API,以使我们的数组索引真正唯一。

\n
    \n
  1. 引入一个自定义集合,\xe2\x80\x99 将另一个集合的索引与其包含的元素的标识符组合起来。
  2. \n
\n
struct IdentifiableIndices<Base: RandomAccessCollection>\nwhere Base.Element: Identifiable {\n    \n    typealias Index = Base.Index\n    \n    struct Element: Identifiable {\n        let id: Base.Element.ID\n        let rawValue: Index\n    }\n    \n    fileprivate var base: Base\n}\n
Run Code Online (Sandbox Code Playgroud)\n
    \n
  1. 使我们的新集合符合标准库\xe2\x80\x99sRandomAccessCollection协议,
  2. \n
\n
extension IdentifiableIndices: RandomAccessCollection {\n    var startIndex: Index { base.startIndex }\n    var endIndex: Index { base.endIndex }\n    \n    subscript(position: Index) -> Element {\n        Element(id: base[position].id, rawValue: position)\n    }\n    \n    func index(before index: Index) -> Index {\n        base.index(before: index)\n    }\n    \n    func index(after index: Index) -> Index {\n        base.index(after: index)\n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n
    \n
  1. IdentifiableIndices通过将以下计算属性添加到所有兼容的基本集合(即支持随机访问并且还包含Identifiable元素的集合),可以轻松创建实例:
  2. \n
\n
extension RandomAccessCollection where Element: Identifiable {\n    var identifiableIndices: IdentifiableIndices<Self> {\n        IdentifiableIndices(base: self)\n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n
    \n
  1. 最后,让\xe2\x80\x99s 还ForEach使用便利的 API 扩展 SwiftUI\xe2\x80\x99s 类型,\xe2\x80\x99s 可以让我们迭代集合,IdentifiableIndices而无需手动访问rawValue每个索引:
  2. \n
\n
extension ForEach where ID == Data.Element.ID,\n                        Data.Element: Identifiable,\n                        Content: View {\n    init<T>(\n        _ data: Binding<T>,\n        @ViewBuilder content: @escaping (T.Index, Binding<T.Element>) -> Content\n    ) where Data == IdentifiableIndices<T>, T: MutableCollection {\n        self.init(data.wrappedValue.identifiableIndices) { index in\n            content(\n                index.rawValue,\n                Binding(\n    get: { data.wrappedValue[index.rawValue] },\n    set: { data.wrappedValue[index.rawValue] = $0 }\n)\n            )\n        }\n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n
    \n
  1. 最后,在您的 中ContentView,您可以更改ForEach为:
  2. \n
\n
ForEach($items) { index, item in\n    TextField("", text: item.name, onCommit: {\n        saveItems()\n    })\n}\n
Run Code Online (Sandbox Code Playgroud)\n

属性@Binding包装器让我们声明一个值实际上来自其他地方,并且应该在两个地方共享。当删除Item列表中的 时,数组items会快速变化,根据我的经验,这会导致问题。\n似乎SwiftUI对它创建的集合绑定应用了某种形式的缓存,这可能会导致在下标到我们的底层时使用过时的索引\xc2\xa0Item \xc2\xa0array \xe2\x80\x94 这会导致应用程序因越界错误而崩溃。

\n