Realm Live Objects: Automatic UI Sync with Zero‑Copy Proxies
Learn how Realm’s Live Objects and auto‑updating Results let you build reactive mobile UIs without manual refreshes, and discover the key trade‑offs to keep your app fast and stable.
31 Aug 2025, 02:22 UTC

The Problem
When building a mobile app, keeping the UI in sync with the underlying data store is a perennial pain point. Developers typically write a query, bind the results to a list, and then remember to re‑query or refresh the UI after every write. This manual bookkeeping is error‑prone and can lead to stale data or UI flicker.
Why Live Objects Matter
Realm solves this by treating every query as a *live view*. The returned Results collection is a dynamic, auto‑updating proxy that reflects the current state of the database without extra queries. Because the objects are zero‑copy proxies, the UI can display the data directly, and any change in the database propagates instantly to the UI.
Zero‑Copy Proxies Explained
A Realm object is not a plain Swift or Kotlin POJO; it is a lightweight proxy that lazily fetches data from the database on demand. This means:
- No need to copy fields into a new object.
- Memory footprint is minimal—only the proxy structure is held in memory.
- Reads are fast because the proxy references the on‑disk data directly.
How Realm Implements Live Views
When you perform a query:
let contacts = realm.objects(Contact.self).filter("age > 20")
The contacts variable is a Results<Contact> live view. Any insert, delete, or update that satisfies the filter automatically updates this collection. Realm also offers fine‑grained notifications:
let notificationToken = contacts.observe { changes in
switch changes {
case .initial:
print("Initial load: \(contacts.count) contacts")
case .update(let added, let removed, let modified, let invalidated):
if !added.isEmpty { print("Added: \(added)") }
if !removed.isEmpty { print("Removed: \(removed)") }
if !modified.isEmpty { print("Modified: \(modified)") }
case .error(let err):
print("Error: \(err)")
}
}
The listener receives only the delta, not the full dataset. This keeps UI updates lightweight.
Practical Example: Swift UI List
Below is a minimal SwiftUI view that displays a list of contacts and updates automatically as the database changes. The example demonstrates thread confinement, listener cleanup, and how to avoid infinite loops.
import SwiftUI
import RealmSwift
struct ContactListView: View {
@StateObject private var viewModel = ContactListViewModel()
var body: some View {
List(viewModel.contacts) { contact in
Text(contact.name)
}
.onAppear { viewModel.startObserving() }
.onDisappear { viewModel.stopObserving() }
}
}
final class ContactListViewModel: ObservableObject {
@Published var contacts: [Contact] = []
private var notificationToken: NotificationToken?
private let realm = try! Realm() // Use the default Realm for simplicity
func startObserving() {
let results = realm.objects(Contact.self).sorted(byKeyPath: "name")
// Observe changes on the main thread
notificationToken = results.observe { [weak self] changes in
guard let self = self else { return }
switch changes {
case .initial:
self.contacts = Array(results)
case .update(let _, let _, let _, _):
self.contacts = Array(results)
case .error(let err):
print("Realm error: \(err)")
}
}
}
func stopObserving() {
notificationToken?.invalidate()
notificationToken = nil
}
}
// Simple Realm model
final class Contact: Object, Identifiable {
@objc dynamic var id = UUID().uuidString
@objc dynamic var name = ""
@objc dynamic var age = 0
override static func primaryKey() -> String? { "id" }
}
Key points:
- The
observecallback runs on the thread where the query was made—here, the main thread. - We never pass a
Contactobject to a background thread; attempting to do so would throw a runtime exception. - When the view disappears we invalidate the token to avoid memory leaks.
Trade‑Offs & Pitfalls
Thread Confined Proxies
Realm objects are *thread‑confined*. A proxy created on the main thread cannot be used on a background thread. If you need to work with data off the UI thread, copy the values into a plain struct or fetch a new proxy on that thread.
Version Pinning & Disk Space
Holding onto many live objects across the lifetime of the Realm can pin older database versions, preventing Realm from reclaiming deleted or overwritten data. To mitigate, keep the scope of live object references short and invalidate listeners when they are no longer needed.
Infinite Notification Loops
Writing to a Realm object on the same thread that is observing it can trigger the listener again, potentially causing a loop. A common pattern is to perform writes inside a realm.write block that is executed only after the listener has processed the previous change, or to use a flag to suppress notifications during the write.
Actionable Takeaways
- Use live queries for any UI component that needs to stay in sync with the database.
- Always observe on the thread you intend to update the UI from; invalidate listeners promptly.
- When passing data to background work, copy the values out of the Realm proxy.
- Monitor the Realm file size; if you notice it growing unexpectedly, check for retained live objects and consider resetting the listener or closing the Realm.
- Test notification behavior by adding a listener that logs each change and performing writes on the same thread to observe potential loops.
Realm’s live objects and auto‑updating results give developers a powerful, low‑overhead way to keep UIs fresh. By understanding the constraints—thread confinement, version pinning, and notification loops—you can harness this feature safely and build responsive, data‑driven mobile experiences.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.