SwiftUI + Realm:从列表中删除一行会导致异常“RLMException”,原因:“对象已被删除或失效。”

Posted

技术标签:

【中文标题】SwiftUI + Realm:从列表中删除一行会导致异常“RLMException”,原因:“对象已被删除或失效。”【英文标题】:SwiftUI + Realm: removing a row from List results in exception 'RLMException', reason: 'Object has been deleted or invalidated.' 【发布时间】:2019-10-14 17:45:32 【问题描述】:

我正在尝试将一个小的 RSS 提要阅读器应用从 UIKit 移植到 SwiftUI,这个应用使用 Realm 进行持久化。

为了让 Realm 在 SwiftUI 中可以绑定,我在我的项目中添加了以下代码:

import Foundation
import SwiftUI
import RealmSwift


final class FeedData: ObservableObject 

    @Published var feeds: [Feed] 
        didSet 
            cleanRealm()
        
    
    private var feedsToken: NotificationToken?

    private func activateFeedsToken() 
        let realm = try! Realm()
        let feeds = realm.objects(Feed.self)
        feedsToken = feeds.observe  _ in
            self.feeds = Array(feeds)
        
    

    func cleanRealm() 
        let realm = try! Realm()
        let tempFeeds = realm.objects(Feed.self)
        let diff = feeds.difference(from: tempFeeds)
        for change in diff 
            switch change 
            case .remove(_, let element, _):
                do 
                    try realm.write 
                        print("Removing \(element.name)")
                        if element.isInvalidated 
                            print("Error: element invalidated.")
                         else 
                            realm.delete(element)
                            print("Removed \(element.name)")
                        
                    
                 catch 
                    fatalError(error.localizedDescription)
                
            default:
                break
            
        
    

    init() 
        let realm = try! Realm()
        feeds = Array(realm.objects(Feed.self))
        activateFeedsToken()
    

    deinit 
        feedsToken?.invalidate()
    

所以,我们有一个带有 DidSet 观察者的 feeds 数组,它在触发时调用 cleanRealm() 函数,然后使用集合差异删除不再在数组中但仍存储在 Realm 中的 Feed 对象 - 我知道这非常笨重,但我认为它至少可以使数组与 Realm 数据库保持同步。

在我的 SwiftUI 视图中,我使用 FeedData 作为 EnvironmentObject 并使用一个 List 显示所有使用 ForEach 的 Feed 对象。

当用户从列表中删除一个提要时,它会从数组中删除,如下所示:

            .onDelete(perform: deleteItems)

然后调用这个函数:

func deleteItems(at offsets: IndexSet) 
    print("Removing feeds at requested offsets.")
    feedData.feeds.remove(atOffsets: offsets)
    print("Removed feeds at requested offsets.")

问题:当我运行我的应用程序然后从列表中删除一个条目时,会引发以下异常:

* 由于未捕获的异常“RLMException”而终止应用程序,原因:“对象已被删除或无效。” * 首先抛出调用栈: (0x1a4a97ab0 0x1a47b1028 0x10098f6a8 0x100996324 0x1009962e8 0x10000dfa4 0x1b215d830 0x1b215d3f0 0x1b215d170 0x1b215f470 0x1db363c4c 0x1db36a0c8 0x1db36a240 0x1db36a6c0 0x1b22d647c 0x1db3ca2c4 0x1db3c9f04 0x1b21a924c 0x1b21a943c 0x1b21a9b50 0x1b22d647c 0x1daefcd50 0x1db3f0ac4 0x1db3eb7b8 0x1db3ead20 0x1db14dca4 0x1db14c5c0 0x1db4d5d0c 0x1db1bdc1c 0x1db1b836c 0x1db1bef70 0x1cf72c9c0 0x1cf713e9c 0x1cf714164 0x1cf719130 0x1db07f9f0 0x1db084e10 0x1db3ac770 0x1db07f96c 0x1cf719284 0x1db081ca0 0x1db081a48 0x1db0816c8 0x1db081834 0x1db3ac770 0x1db0817fc 0x1db09f848 0x1daf00a10 0x1daf00970 0x1daf00a8c 0x1a4a12668 0x1a4a0d308 0x1a4a0d8b8 0x1a4a0d084 0x1aec56534 0x1a8b7b8b8 0x10004e520 0x1a488ce18) libc++abi.dylib:以 NSException 类型的未捕获异常终止

到目前为止,我的研究表明,当 Realm 对象被删除时,它会在被完全删除之前发生变异。

所以,我认为这里可能发生的情况是,当对象在从 Realm 中删除之前发生突变时,SwiftUI 会检测到此更改,重绘视图,然后尝试访问现在无效的 Realm 对象,从而引发异常.

Realm 对象确实有一个 isInvalidated 属性,我可能应该在将其添加到列表之前检查它,但是 AFAIK(请随时纠正我)在 ForEach 块中没有办法检查这种情况如果需要,可以“继续”到下一个数组项。

非常感谢您对此的任何帮助,我整天都在处理这个问题,只是找不到一个好的解决方案,这可能很明显,但我还在学习 SwiftUI 在工作时可以做的所有美妙的事情这个示例项目。

谢谢!

更新:

按照 Jay 的建议,我设法修改了我的 FeedData 类,以便它公开一个 Realm Results 属性供我的应用使用,而不是复制到单独的数组中。

这也意味着我在删除提要时不再需要进行任何集合比较,但由于 SwiftUI 的 .onDelete(perform:) 修饰符需要一个接受 IndexSet 的函数,所以我的删除函数仍然是一个小技巧。

更新后的 FeedData 类现在如下所示:

import Foundation
import SwiftUI
import RealmSwift


final class FeedData: ObservableObject 

    @Published var feeds: Results<Feed>
    private var feedsToken: NotificationToken?

    private func activateFeedsToken() 
        let realm = try! Realm()
        let feeds = realm.objects(Feed.self)
        feedsToken = feeds.observe  _ in
            self.feeds = feeds
        
    

    func deleteItems(at offsets: IndexSet) 
        print("deleteItems called.")
        let realm = try! Realm()

        do 
            try realm.write 
                offsets.forEach  index in
                    print("Attempting to access index \(index).")
                    if index < feeds.count 
                        print("Index is valid.")
                        let item = feeds[index]
                        print("Removing \(item.name)")
                        realm.delete(item)
                        print("Removed item.")
                    
                
            
         catch 
            fatalError(error.localizedDescription)
        
    
    init() 
        let realm = try! Realm()
        feeds = realm.objects(Feed.self)
        activateFeedsToken()
    

    deinit 
        feedsToken?.invalidate()
    

虽然这在技术上可行,但我现在的问题是,一旦从领域中删除对象(通过使用断点进行测试),我的视图的 ForEach() 方法就会被调用,这甚至发生在领域通知被调度之前。

这会导致尝试访问已删除对象的索引,在这种情况下,应用程序将崩溃,并出现来自 Realm 的索引越界错误。

这应该是一个单独的问题,因为我原来的问题已经解决了。

@Jay,您能否将您的第一条评论标记为对我问题的回答,以便我批准?

感谢您的大力帮助!

【问题讨论】:

如果不深入研究代码,我会考虑另一种方法。 Realm 对象是活动对象 - 当从 Realm 中删除对象时,任何知道该对象的结果也会删除该对象。通过观察这些结果,应用程序会收到删除通知,并可以相应地更新 UI。例如,比较移除不再在数组中的 Feed 对象 是不需要的,因为当从 Realm 中删除对象时,它将不再出现在结果中。如果要将结果复制到数组中,则可能需要使用结果本身。 感谢 Jay,我完全同意您的观点,即直接使用 Realm 的 Results 类型而不需要将包含的项目复制到数组中的额外步骤将是一种更好、更明智的方法。然而不幸的是,无论出于何种原因,这似乎都不起作用,Xcode 会一直告诉我 Feed 不包含 ID 属性,尽管它完全包含并且我在 ForEach 调用中指定了它。如果我能成功,我会再试一次并接受你的回答。 有些事情:没有说明 ID 属性是什么、在哪里使用或与问题中的代码有什么关系。我没有看到任何使用 ForEach 的东西。重要的是要记住,当您从 Realm 中删除对象时,引用该对象的所有结果也将不再具有该对象。因此,如果您删除索引 4 处的对象,然后尝试删除该对象,它将已被删除且无效。我们不知道feedData.feeds.remove 做了什么或这些对象是什么,但这可能与它有关。如果 feedData 是一个结果对象,则会抛出该错误。 另外,仅仅因为 Xcode 说 Feed 不包含 ID 属性,并不意味着这是错误。出于某种原因,Feed 不包含 ID 属性肯定是真的,但是在当前状态下的 Xcode 仍然喜欢通过给出与代码中的实际错误完全无关的无关错误来抱怨 ForEach 内部的错误。有关我的意思的示例,请参阅我在另一篇文章中给出的答案:***.com/a/58372096/5140471 如果您删除了一个领域对象,您将无法引用它,否则您将遇到上述异常。要避免此问题,请通过创建属性 isDeleted 并将其设置为 true 并在查询过滤器中使用 isDeleted 来使用软删除。如果您尝试使用 NSOutlineView 会遇到同样的问题,因为 NSOutlineView 持有对对象的引用,并且在您删除似乎尝试引用领域对象的对象时会执行“填充”。或者使用包装类来包含领域对象,然后在尝试任何操作之前检查领域对象是否已失效。 【参考方案1】:

我也遇到了这个问题。不知道发生了什么,我认为是由 SwiftUI 引起的一些复杂的多次调用行为。当我扩展我的 notificationToken 观察块并打开更改以在调用删除时返回时,它工作正常。在你的情况下:

    feedsToken = feeds.observe  [weak self] (changes: RealmCollectionChange) in
        switch changes 
        case .initial:
            print("Initial call")
        case .update(_, let deletions, _, _):
            print("Updates made!")
            if !deletions.isEmpty  return 
            self.feeds = feeds
        case .error(let error):
            print("Error observing changes")
        
    

【讨论】:

以上是关于SwiftUI + Realm:从列表中删除一行会导致异常“RLMException”,原因:“对象已被删除或失效。”的主要内容,如果未能解决你的问题,请参考以下文章

如何从 SwiftUI 和 Realm 中另一个列表中的对象中添加和删除列表中的对象

从 SwiftUI 列表和领域中删除记录时出错

在 SwiftUI 列表中呈现来自 Realm 的数据的正确方法是啥

在 SwiftUI 列表中呈现来自 Realm 的数据的正确方法是啥

为啥从 Realm DB 加载图像时 SwiftUI List 会崩溃? RAM 达到极限

macOS 上的 SwiftUI:如何为 onDelete 启用 UI(从列表中删除)