将Realm与SwiftUI结合使用时索引超出范围

时间:2019-07-23 09:20:05

标签: ios swift realm swiftui

我一直在使用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)

不太漂亮,但是直到我找到崩溃的真正原因为止,这项工作才能完成。

2 个答案:

答案 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) })
    }
}