Realm Live Objects: Why Your UI Can Stop Manually Refreshing Local Data
Realm's live objects and auto-updating Results eliminate manual refresh logic for local persistence, but thread confinement and notification token lifecycle require deliberate handling.
03 Apr 2026, 00:24 UTC

If you have ever built a local-first mobile feature, you know the drill: query the database, map the rows into view models, render the list, and then carefully orchestrate a reload whenever another part of the app writes to storage. The manual refresh loop is a tax you pay on every screen. Realm offers a different contract. In Realm, managed objects are live views of the underlying data, and Results collections are auto-updating. That means your UI can observe the database directly instead of maintaining a parallel copy of state.
This article looks at how Realm's live object model changes reactive UI development, where it removes boilerplate, and where it introduces constraints you must respect.
Live objects are views, not snapshots
In many object-relational mappers, a fetch returns a detached copy. You edit the copy, save it back, and hope your in-memory representation matches the database. Realm inverts that. When you retrieve a managed object, you receive a handle to the live record. If another thread commits a write that affects that record, the same instance reflects the new state on the next access. You do not re-fetch, and you do not rebuild your view model.
The same principle applies to Results. A Results instance is not a static array; it is a live query. If the underlying objects change so that they now match or no longer match the query criteria, the collection updates itself. Insertions, deletions, and modifications are reflected in the collection without re-executing the query.
Auto-updating Results and notification tokens
Because Results update automatically, the remaining problem is telling your UI what changed so it can animate a table view or refresh a specific widget. Realm solves this with notification tokens. A token is an observer you attach to a collection or an individual object. When a change occurs, Realm calls your handler with precise indices: which rows were inserted, deleted, or modified.
This replaces the manual diffing and state-synchronization logic that often grows around local persistence. You no longer need a Redux-style store or a hand-rolled change tracker just to keep a list in sync with disk.
A worked example: observing a task list
The following Swift example illustrates the pattern. It assumes a recent Realm Swift SDK (10.x or later) where observe and ThreadSafeReference are available.
// Run on the main thread, e.g. in a UIViewController
let realm = try! Realm()
let incompleteTasks = realm.objects(Task.self).filter("isCompleted == false")
// Keep a strong reference to the token; it must outlive the observation
notificationToken = incompleteTasks.observe { changes in
switch changes {
case .initial:
tableView.reloadData()
case .update(_, let deletions, let insertions, let modifications):
tableView.performBatchUpdates {
tableView.deleteRows(at: deletions.map { IndexPath(row: $0, section: 0) }, with: .automatic)
tableView.insertRows(at: insertions.map { IndexPath(row: $0, section: 0) }, with: .automatic)
tableView.reloadRows(at: modifications.map { IndexPath(row: $0, section: 0) }, with: .automatic)
}
case .error(let error):
// Handle observation failure
print("Observation error: \(error)")
}
}
// Background write: run on a background queue, never the main thread
DispatchQueue.global().async {
guard let realm = try? Realm() else { return }
// If you have a primary key, re-query instead of passing the object
guard let task = realm.object(ofType: Task.self, forPrimaryKey: taskId) else { return }
try! realm.write {
task.isCompleted = true
}
}
Where to run this: The observation block and Results query belong on the thread that owns the UI, typically the main thread. The write block belongs on a background queue.
Expected check: When the background write commits, the .update case fires on the main thread with the exact index of the deleted row. You should not call reloadData() manually.
Relevant risk: If you forget to store notificationToken as a property, the token is deallocated immediately and notifications stop. If you never invalidate it, it leaks.
The thread-confinement boundary
Realm enforces thread confinement: a managed object may only be accessed on the thread that created it. If you capture incompleteTasks or any Task instance inside the background closure and touch it there, Realm throws a runtime exception. To move an object reference across threads, wrap it in a ThreadSafeReference and resolve it on the destination thread. In other Realm SDKs, the equivalent is re-querying by primary key on the target thread.
A useful diagnostic is to intentionally pass a managed object to a background worker in a debug build. If the app does not crash, you are likely working with an unmanaged copy, not a Realm-managed object.
Trade-offs and limitations
- Token lifecycle leaks. Notification tokens hold references until invalidated. If you create them inside a method without storing them, or if you forget to call
invalidate()indeinit, they accumulate. Always tie token lifetime to the lifecycle of the observing view or view model. - UI stuttering on large collections. A live
Resultsobserved on the main thread is convenient, but heavy write churn on a background thread can flood the main thread with change notifications. If your collection is large, paginate or slice the query (for example, usingprefix(n)) so the UI only observes a bounded window. - Different mental model. Developers accustomed to value-type ORMs or immutable snapshots must adjust. Realm objects are mutable views. Accidental writes inside a notification handler, or assumptions that an object is a detached copy, can introduce subtle bugs.
Actionable checks
- Verify auto-update. Query a
Resultscollection on the main thread, then modify a matching record from a background queue. Confirm that the collection and your notification handler update without an explicit re-query. - Verify thread confinement. In a debug build, attempt to access a managed object on a thread other than its creator. Confirm that Realm throws, then refactor the code to use
ThreadSafeReferenceor a primary-key re-query. - Verify token cleanup. Add logging to your view controller's
deinitand ensure that every notification token is invalidated when the observer goes away.
Realm's live object model does not remove all state management, but it removes the mechanical part: the re-query, the diff, and the manual reload. What remains is a smaller, more deliberate set of concerns—thread boundaries and observer lifetime—that you can verify with targeted tests rather than broad architectural scaffolding.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.