我一直在尝试使用 SwiftUI,并一直在编写一个小型膳食计划/待办事项列表样式的应用程序。 我能够让 Realm 与 SwiftUI 一起工作,并编写了一个小包装对象来获取 Realm 更改通知以更新 UI。 这对于添加项目非常有用,并且 UI 会得到正确更新。但是,当使用滑动删除或其他方法删除项目时,我从 Realm 收到索引越界错误。
这是一些代码:
内容 View :
struct ContentView : View {
@EnvironmentObject var userData: MealObject
@State var draftName: String = ""
@State var isEditing: Bool = false
@State var isTyping: Bool = false
var body: some View {
List {
HStack {
TextField($draftName, placeholder: Text("Add meal..."), onEditingChanged: { editing in
self.isTyping = editing
},
onCommit: {
self.createMeal()
})
if isTyping {
Button(action: { self.createMeal() }) {
Text("Add")
}
}
}
ForEach(self.userData.meals) { meal in
NavigationLink(destination: DetailMealView(ingredientsObject: IngredientsObject(meal: meal))) {
MealRow(name: meal.name)
}
}.onDelete(perform: delete)
}
.navigationBarTitle(Text("Meals"))
}
func delete(at offsets: IndexSet) {
guard let index = offsets.first else {
return
}
let mealToDelete = userData.meals[index]
Meal.delete(meal: mealToDelete)
print("Meals after delete: \(self.userData.meals)")
}
}
以及 MealObject 包装类:
final class MealObject: BindableObject {
let willChange = PassthroughSubject<MealObject, Never>()
private var token: NotificationToken!
var meals: Results<Meal>
init() {
self.meals = Meal.all()
lateInit()
}
func lateInit() {
token = meals.observe { changes in
self.willChange.send(self)
}
}
deinit {
token.invalidate()
}
}
我能够将问题范围缩小到
ForEach(self.userData.meals) { meal in
NavigationLink(destination: DetailMealView(ingredientsObject: IngredientsObject(meal: meal))) {
MealRow(name: meal.name)
}
}
似乎 self.userData.meals 没有更新,即使在检查 MealObject 中的更改通知时,它显示了正确的删除,并且 MealObject 中的膳食变量也正确更新。
*编辑:还要补充一点,删除确实发生了,当再次启动应用程序时,删除的项目消失了。 SwiftUI 似乎对状态感到困惑,并在调用 willChange 后尝试访问已删除的项目。
*编辑2:现在找到了一个解决方法,我实现了一种检查对象当前是否存在于Realm中的方法:
static func objectExists(id: String, in realm: Realm = try! Realm()) -> Bool {
return realm.object(ofType: Meal.self, forPrimaryKey: id) != nil
}
这样调用
ForEach(self.userData.meals) { meal in
if Meal.objectExists(id: meal.id) {
NavigationLink(destination: DetailMealView(ingredientsObject: IngredientsObject(meal: meal))) {
MealRow(name: meal.name)
}
}
}.onDelete(perform: delete)
不是很漂亮,但它可以完成工作,直到我找到崩溃的真正原因。
最佳答案
使用 Realm Cocoa 5.0,您现在只需卡住传递给 ForEach
的任何集合:
ForEach(self.userData.meals.freeze()) { meal in
NavigationLink(destination: DetailMealView(ingredientsObject: IngredientsObject(meal: meal))) {
MealRow(name: meal.name)
}
}
<小时/>
5.0 之前的答案:
SwiftUI 的 ForEach 的工作原理是,在发送 objectWillChange() 之后,它会迭代之前给出的集合和给出的新集合,然后对它们进行比较。这仅适用于不可变集合,但 Realm 集合是可变且实时更新的。此外,集合中的对象也会发生变化,因此将集合复制到数组中的明显解决方法也无法完全发挥作用。
我想出的最佳解决方法如下:
// helpers
struct ListKey {
let id: String
let index: Int
}
func keyedEnumeration<T: Object>(_ results: Results<T>) -> [ListKey] {
return Array(results.value(forKey: "id").enumerated().map { ListKey(id: $0.1 as! String, index: $0.0) })
}
// in the body
ForEach(keyedEnumeration(self.userData.meals), id: \ListKey.id) { key in
let meal = self.userData.meals[key.index]
NavigationLink(destination: DetailMealView(ingredientsObject: IngredientsObject(meal: meal))) {
MealRow(name: meal.name)
}
}
这里的想法是预先提取主键数组并将其提供给 SwiftUI,以便它可以在不接触 Realm 的情况下区分它们,而不是尝试从实际已更新的“旧”集合中读取。
Realm 的 future 版本将支持卡住集合/对象,这将更适合 SwiftUI 想要的语义,但没有预计到达时间。
关于ios - 将 Realm 与 SwiftUI 结合使用时索引越界,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/57160790/