将 Realm 与 SwiftUI 结合使用时索引越界

Luc*_*uca 11 realm ios swift swiftui

我一直在玩 SwiftUI,并且一直在编写一个小型膳食计划/待办事项列表样式的应用程序。我能够让 Realm 与 SwiftUI 一起工作,并编写了一个小的包装器对象来获取 Realm 更改通知以更新 UI。这对于添加项目和 UI 得到正确更新非常有用。但是,当使用滑动删除或其他方法删除项目时,我从 Realm 收到索引越界错误。

这是一些代码:

内容视图:

    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)")
    }
}
Run Code Online (Sandbox Code Playgroud)

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

Run Code Online (Sandbox Code Playgroud)

我能够将问题缩小到

   ForEach(self.userData.meals) { meal in
      NavigationLink(destination: DetailMealView(ingredientsObject: IngredientsObject(meal: meal))) {
      MealRow(name: meal.name)
     }
   }
Run Code Online (Sandbox Code Playgroud)

似乎 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
    }

Run Code Online (Sandbox Code Playgroud)

这样叫

            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)
Run Code Online (Sandbox Code Playgroud)

不是很漂亮,但它可以完成工作,直到我找到崩溃的真正原因。

Tho*_*yne 5

使用 Realm Cocoa 5.0,您现在只需冻结传递给的任何集合ForEach

ForEach(self.userData.meals.freeze()) { meal in
    NavigationLink(destination: DetailMealView(ingredientsObject: IngredientsObject(meal: meal))) {
        MealRow(name: meal.name)
    }
}
Run Code Online (Sandbox Code Playgroud)

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)
    }
}
Run Code Online (Sandbox Code Playgroud)

这里的想法是预先提取主键数组并将其提供给 SwiftUI,以便它可以区分它们而无需接触 Realm,而不是尝试从实际已更新的“旧”集合中读取。

Realm 的未来版本将支持冻结集合/对象,这将更适合 SwiftUI 想要的语义,但没有预计到达时间。