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!