将Realm与SwiftUI结合使用时索引超出范围 [英] Index out of bounds when using Realm with SwiftUI

查看:106
本文介绍了将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之后尝试访问已删除的项目.

*现在找到了一种解决方法,我实现了一种检查对象是否存在于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)
 

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

解决方案

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.

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.

Here's some code:

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)")
    }
}

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

I was able to narrow the issue down to

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

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.

*Edit: 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: 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
    }

Called like this

            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.

解决方案

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

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.

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天全站免登陆