How to display Realm Results in SwiftUI List?

The data that you pass in List or a ForEach must conform to the Identifiable protocol.

Either you adopt it in your Realm models or you use .identified(by:) method.


Even with that, the View won't reload if the data changes.

You could wrap Results and make it a BindableObject, so the view can detect the changes and reload itself:

class BindableResults<Element>: ObservableObject where Element: RealmSwift.RealmCollectionValue {

    var results: Results<Element>
    private var token: NotificationToken!

    init(results: Results<Element>) {
        self.results = results
        lateInit()
    }

    func lateInit() {
        token = results.observe { [weak self] _ in
            self?.objectWillChange.send()
        }
    }

    deinit {
        token.invalidate()
    }
}

And use it like:

struct ContentView : View {

    @ObservedObject var dogs = BindableResults(results: try! Realm().objects(Dog.self))

    var body: some View {
        List(dogs.results.identified(by: \.name)) { dog in
            DogRow(dog: dog)
        }
    }

}

I have created a generic solution to display and add/delete for any Results<T>. By default, Results<T> is "live". SwiftUI sends changes to View when the @Published property WILL update. When a RealmCollectionChange<Results<T>> notification is received, Results<T> has already updated; Therefore, a fatalError will occur on deletion due to index out of range. Instead, I use a "live" Results<T> for tracking changes and a "frozen" Results<T> for use with the View. A full working example, including how to use a generic View with RealmViewModel<T> (shown below), can be found here: SwiftUI+Realm. The enum Status is used to display a ProgressView, "No records found", etc., when applicable, as shown in the project. Also, note that the "frozen" Results<T> is used when needing a count or single object. When deleting, the IndexSet by onDelete is going to return a position from the "frozen" Results<T> so it checks that the object still existing in the "live" Results<T>.

class RealmViewModel<T: RealmSwift.Object>: ObservableObject, Verbose where T: Identifiable {

typealias Element = T

enum Status {
    // Display ProgressView
    case fetching
    // Display "No records found."
    case empty
    // Display results
    case results
    // Display error
    case error(Swift.Error)
    
    enum _Error: String, Swift.Error {
        case fetchNotCalled = "System Error."
    }
}

init() {
    fetch()
}

deinit {
    token?.invalidate()
}

@Published private(set) var status: Status = .error(Status._Error.fetchNotCalled)

// Frozen results: Used for View

@Published private(set) var results: Results<Element>?

// Live results: Used for NotificationToken

private var __results: Results<Element>?

private var token: NotificationToken?

private func notification(_ change: RealmCollectionChange<Results<Element>>) {
    switch change {
        case .error(let error):
            verbose(error)
            self.__results = nil
            self.results = nil
            self.token = nil
            self.status = .error(error)
        case .initial(let results):
            verbose("count:", results.count)
            //self.results = results.freeze()
            //self.status = results.count == 0 ? .empty : .results
        case .update(let results, let deletes, let inserts, let updates):
            verbose("results:", results.count, "deletes:", deletes, "inserts:", inserts, "updates:", updates)
            self.results = results.freeze()
            self.status = results.count == 0 ? .empty : .results
    }
}

var count: Int { results?.count ?? 0 }

subscript(_ i: Int) -> Element? { results?[i] }

func fetch() {
    
    status = .fetching
    
    //Realm.asyncOpen(callback: asyncOpen(_:_:))
    
    do {
        let realm = try Realm()
        let results = realm.objects(Element.self).sorted(byKeyPath: "id")
        self.__results = results
        self.results = results.freeze()
        self.token = self.__results?.observe(notification)
        
        status = results.count == 0 ? .empty : .results
        
    } catch {
        verbose(error)
        
        self.__results = nil
        self.results = nil
        self.token = nil
        
        status = .error(error)
    }
}

func insert(_ data: Element) throws {
    let realm = try Realm()
    try realm.write({
        realm.add(data)
    })
}

func delete(at offsets: IndexSet) throws {
    let realm = try Realm()
    try realm.write({
        
        offsets.forEach { (i) in
            guard let id = results?[i].id else { return }
            guard let data = __results?.first(where: { $0.id == id }) else { return }
            realm.delete(data)
        }
    })
}

}


This is the most straight forward way of doing it:

struct ContentView: View {
    @State private var dog: Results<Dog> = try! Realm(configuration: Realm.Configuration(schemaVersion: 1)).objects(Dog.self)

    var body: some View {
        ForEach(dog, id: \.name) { i in
        Text(String((i.name)!))
        }
    }
}

...That's it, and it works!