将 Realm 与 SwiftUI 结合使用时索引越界 [英] Index out of bounds when using Realm with SwiftUI

查看:26
本文介绍了将 Realm 与 SwiftUI 结合使用时索引越界的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

I've been playing around with SwiftUI a bit and have been writing a small meal planner/todo list style app. I was able to get Realm working with SwiftUI and wrote a small wrapper object to get Realm change notifications to update the UI. This works great for adding items and the UI gets properly updated. However, when deleting an item using swipe to delete or other methods, I get an index out of bounds error from 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)")
    }
}

还有 MealObject 包装类:

And the MealObject wrapper class:

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 中的膳食变量也正确更新.

It seems like self.userData.meals isn't updating, even though when checking the change notification in MealObject it shows the correct deletions and the meals variable in MealObject correctly updates as well.

*另外要补充的是,删除确实发生了,再次启动应用程序时,删除的项目消失了.SwiftUI 似乎对状态感到困惑,并在调用 willChange 后尝试访问已删除的项目.

* Also to add, the deletion does actually happen and when launching the app again, the deleted item is gone. It seems like SwiftUI gets confused about the state and tries to access the deleted item after willChange gets called.

*Edit 2: 现在找到了一个解决方法,我实现了一个检查对象当前是否存在于 Realm 的方法:

*Edit 2: Found one workaround for now, I implemented a method checking whether the object currently exists in 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)

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

Not very pretty but it gets the job done until I find the real cause for the crash.

推荐答案

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

With Realm Cocoa 5.0, you now just need to freeze whatever collection you pass to ForEach:

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


5.0 之前的答案:


Pre 5.0 answer:

SwiftUI 的 ForEach 似乎是如何工作的,在收到 objectWillChange() 之后,它会遍历之前给定的集合和给定的新集合,然后对它们进行比较.这仅适用于不可变集合,但 Realm 集合是可变的和实时更新的.此外,集合中的对象也会发生变化,因此将集合复制到数组中的明显解决方法也不能完全奏效.

How SwiftUI's ForEach appears to work is that after getting sent objectWillChange() it iterates over the collection it was previously given and the new collection it is given, and then diffs them. This only works properly for immutable collections, but Realm collections are mutable and live-updating. In addition, the objects in the collection also change, so the obvious workaround of copying the collection into an Array doesn't full work either.

我想出的最佳解决方法如下:

The best workaround I've come up with is something like the following:

// 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 的情况下对它们进行区分,而不是尝试从旧"版本中读取.实际更新的集合.

The idea here is to extract the array of primary keys up front and give that to SwiftUI so that it can diff them without having to touch Realm, rather than trying to read from the "old" collection that's actually been updated.

Realm 的未来版本将支持冻结的集合/对象,这将更适合 SwiftUI 想要的语义,但没有 ETA.

A future version of Realm will have support for frozen collections/objects that'll be a better fit for the semantics that SwiftUI wants, but no ETA on that.

这篇关于将 Realm 与 SwiftUI 结合使用时索引越界的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆