swiftswiftuirealm

Delete Realm object shown in UI without "invalid object" exception


I am showing a list of objects from my Realm database within my UI:

struct EntriesListView: View {
    @StateObject private var model = Model()
    
    var body: some View {
        VStack {
            List {
                ForEach($model.entries, id: \.self) { entry in
                    Text("\(entry.name)")
                }
            }
            Button("Add dummy entry") {
                Task {
                    await model.addDummyEntry()
                }
            }
            Button("Delete all") {
                Task {
                    await model.deleteAll()
                }
            }
        }.onAppear {
            self.model.load()
        }.onDisappear {
            self.model.unload()
        }
    }
}

Model:

@MainActor public class Model: ObservableObject {
    var realm: Realm?
    
    @Published var entries: [EntryRealmModel] = []
    var entriesToken: NotificationToken? = nil
    
    init() {
        do {
            realm = try Realm()
        } catch {
            realm = nil
        }
    }
    
    private func loadEntries() -> [EntryRealmModel] {
        guard let realm = realm else { return [] }
        let results = realm.objects(EntryRealmModel.self)
        return Array(results)
    }
    
    func load() {
        guard let realm = realm else { return }
        self.entries = self.loadEntries()
        
        entriesToken = realm.observe { (notification, realm) in
            self.entries = self.loadEntries()
        }
    }
    
    func unload() {
        entries = []
        entriesToken?.invalidate()
    }
    
    func addDummyEntry() async {
        guard let realm = realm else { return }
        
        let entry = EntryRealmModel(
            // ...
        )
        
        do {
            try await realm.asyncWrite {
                realm.add(entry)
            }
        } catch { }
    }
    
    func deleteAll() async {
        guard let realm = realm else { return }
        
        do {
            try await realm.asyncWrite {
                realm.delete(realm.objects(EntryRealmModel.self))
            }
        } catch { }
    }
}

The idea is taken from this question.

Adding works fine, entries immediately show up in the list.

But as when I delete all entries, the app crashes with:

*** Terminating app due to uncaught exception 'RLMException', reason: 'Object has been deleted or invalidated.'

Where could this error come from? I thought that the realm.observe takes care of loading the new contents after deleting.

BTW, you would think that the error comes from a faulty reference in Text("\(entry.name)"), but even with that commented out it crashes.


Solution

  • The problematic code is this

    private func loadEntries() -> [EntryRealmModel] {
       guard let realm = realm else { return [] }
       let results = realm.objects(EntryRealmModel.self)
       return Array(results) //<- causing the issue
    }
    

    Realm Results collections are memory friendly, lazy-loading and auto-updating. What that means is if a Realm object added, edited or deleted the Results object will always update to reflect the state of the underlying data.

    If Realm SwiftUI syntax is used, it will, for example cause the view to refresh when that occurs.

    But...

    If those Realm objects are cast to a Swift Array... like this

    return Array(results)
    

    ...all of that functionality goes out the window.

    In this case, if an object is deleted from Realm, the corresponding element in the array will go to NIL, leading to a SwiftUI crash.

    Even using ObservableObject does not work well with Realm objects - it's Swift construct to be used to Swift objects

    The other secondary issue is that Results objects are super memory-friendly because they are all lazily-loaded; meaning that thousands of objects have very little memory impact.

    As soon as they are cast to an array though, all of that memory is gobbled up and a large dataset can overwhelm the device, causing weird, intermittent crashes. (it's also a lot slower)

    The fix? Don't cast to a Swift array. Use the Results object as the datasource - there's a bunch of documentation on implementation here

    Realm SwiftUI

    Implement @ObservedRealmObject and @ObservedResults and it will work wonders for your app.