首页 > 解决方案 > SwiftUI + Realm:从列表中删除一行会导致异常“RLMException”,原因:“对象已被删除或无效。”

问题描述

我正在尝试将一个小的 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”而终止应用程序,原因:“对象已被删除或无效。” *First throw call stack: (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: terminating with uncaught exception of type NSException

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

因此,我认为这里可能发生的情况是,当对象在从 Realm 中删除之前发生突变时,SwiftUI 会检测到此更改,重绘视图,然后尝试访问现在无效的 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,您能否将您的第一条评论标记为对我问题的回答,以便我批准?

感谢您的大力帮助!

标签: swiftrealmswiftui

解决方案


我也遇到了这个问题。不知道发生了什么,我认为是由 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")
        }
    }

推荐阅读