使用 onDelete 时索引超出范围
Getting Index out of range when using onDelete
当我尝试删除列表中的项目时出现以下错误:
Swift/ContiguousArrayBuffer.swift:580: 致命错误:索引超出范围
2021-05-07 09:59:38.171277+0200 部分[4462:220358] Swift/ContiguousArrayBuffer.swift:580:致命错误:索引超出范围
我一直在寻找解决方案,但我发现的 none 似乎适用于我的代码。
仅供参考,这是我在 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 { [=10=].id == id }
}
}
用户默认值:
//
// 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.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
中不再是问题,因为 List
现在支持 binding
这似乎确实是 SwiftUI
本身的一个错误。
经过研究,发现问题出在TextField
绑定上。如果将 TextField
替换为简单的 Text
视图,一切都会正常工作。看起来在删除项目后,TextField
绑定试图访问 deleted
项目,但找不到它导致崩溃。
这篇文章帮助我解决了这个问题,查看 SwiftbySundell
所以要解决这个问题,我们将不得不更深入地研究 Swift 的集合 APIs 以使我们的数组索引真正独一无二。
- 引入一个自定义集合,它将另一个集合的索引与其包含的元素的标识符结合起来。
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
协议,
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)
}
}
- 通过将以下计算的 属性 添加到所有兼容的基础集合(即支持随机访问且还包含
Identifiable
的基础集合,可以轻松创建 IdentifiableIndices
实例元素):
extension RandomAccessCollection where Element: Identifiable {
var identifiableIndices: IdentifiableIndices<Self> {
IdentifiableIndices(base: self)
}
}
- 最后,让我们也扩展 SwiftUI 的
ForEach
类型,方便 API 让我们迭代 IdentifiableIndices
集合无需手动访问每个索引的 rawValue
:
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] = [=13=] }
)
)
}
}
}
- 最后,在您的
ContentView
中,您可以将 ForEach
更改为:
ForEach($items) { index, item in
TextField("", text: item.name, onCommit: {
saveItems()
})
}
@Binding
属性 包装器让我们声明一个值实际上来自其他地方,并且应该在两个地方共享。在我们的列表中删除 Item
时,数组 items
变化很快,根据我的经验,这会导致问题。
SwiftUI
似乎对它创建的集合绑定应用了某种形式的缓存,这可能会导致在订阅我们的底层 Item
数组时使用过时的索引——这会导致应用程序崩溃并显示越界错误。
当我尝试删除列表中的项目时出现以下错误:
Swift/ContiguousArrayBuffer.swift:580: 致命错误:索引超出范围 2021-05-07 09:59:38.171277+0200 部分[4462:220358] Swift/ContiguousArrayBuffer.swift:580:致命错误:索引超出范围
我一直在寻找解决方案,但我发现的 none 似乎适用于我的代码。
仅供参考,这是我在 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 { [=10=].id == id }
}
}
用户默认值:
//
// 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.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
中不再是问题,因为 List
现在支持 binding
这似乎确实是 SwiftUI
本身的一个错误。
经过研究,发现问题出在TextField
绑定上。如果将 TextField
替换为简单的 Text
视图,一切都会正常工作。看起来在删除项目后,TextField
绑定试图访问 deleted
项目,但找不到它导致崩溃。
这篇文章帮助我解决了这个问题,查看 SwiftbySundell
所以要解决这个问题,我们将不得不更深入地研究 Swift 的集合 APIs 以使我们的数组索引真正独一无二。
- 引入一个自定义集合,它将另一个集合的索引与其包含的元素的标识符结合起来。
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
协议,
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)
}
}
- 通过将以下计算的 属性 添加到所有兼容的基础集合(即支持随机访问且还包含
Identifiable
的基础集合,可以轻松创建IdentifiableIndices
实例元素):
extension RandomAccessCollection where Element: Identifiable {
var identifiableIndices: IdentifiableIndices<Self> {
IdentifiableIndices(base: self)
}
}
- 最后,让我们也扩展 SwiftUI 的
ForEach
类型,方便 API 让我们迭代IdentifiableIndices
集合无需手动访问每个索引的rawValue
:
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] = [=13=] }
)
)
}
}
}
- 最后,在您的
ContentView
中,您可以将ForEach
更改为:
ForEach($items) { index, item in
TextField("", text: item.name, onCommit: {
saveItems()
})
}
@Binding
属性 包装器让我们声明一个值实际上来自其他地方,并且应该在两个地方共享。在我们的列表中删除 Item
时,数组 items
变化很快,根据我的经验,这会导致问题。
SwiftUI
似乎对它创建的集合绑定应用了某种形式的缓存,这可能会导致在订阅我们的底层 Item
数组时使用过时的索引——这会导致应用程序崩溃并显示越界错误。