我一直在使用SwiftUI,并且一直在编写一个小型的膳食计划器/待办事项列表样式的应用程序。 我能够让Realm使用SwiftUI,并编写了一个小型包装对象来获取Realm更改通知以更新UI。 这对于添加项目非常有用,并且UI会正确更新。但是,当使用滑动删除或其他方法删除项目时,我收到了Realm的索引超出范围错误。
以下是一些代码:
ContentView:
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)
不太漂亮,但是直到我找到崩溃的真正原因为止,这项工作才能完成。
答案 0 :(得分:3)
SwiftUI的ForEach看起来是如何工作的,是在发送objectWillChange()之后,它遍历先前给出的集合以及给出的新集合,然后进行比较。这仅适用于不可变的集合,但是Realm集合是可变的且实时更新。此外,集合中的 对象也发生了变化,因此将集合复制到Array中的明显解决方法也无法完全起作用。
我想出的最佳解决方法如下:
// 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,而不是尝试从实际上已更新的“旧”集合中读取。 / p>
Realm的未来版本将支持冻结的集合/对象,这些集合/对象将更适合SwiftUI想要的语义,但对此没有ETA。
答案 1 :(得分:0)
我根据托马斯的回答创建了一个辅助函数。这样,您可以将所有ForEach引用更改为ForEach:
public struct ForEachRealm<C: RandomAccessCollection, T: Object, Content: View>: View where C.Element == T, T: Identifiable, C.Index == Int {
let results: C
let fn: (Int, T) -> Content
public init(_ results: C, @ViewBuilder fn: @escaping (T) -> Content) {
self.results = results
self.fn = { _, object in return fn(object) }
}
public init(_ results: C, @ViewBuilder fn: @escaping (Int, T) -> Content) {
self.results = results
self.fn = fn
}
public var body: some View {
ForEach(keyedEnumeration(results)) { holder -> Content in
return self.fn(holder.index, self.results[holder.index])
}
}
// From https://stackoverflow.com/questions/57160790/index-out-of-bounds-when-using-realm-with-swiftui
struct ListKey<T: Identifiable>: Identifiable {
let id: T.ID
let index: Int
}
func keyedEnumeration(_ results: C) -> [ListKey<T>] {
return Array(results.enumerated().map { ListKey(id: $0.element.id, index: $0.offset) })
}
}