Docs Menu

Docs HomeRealm

Actor-Isolated Realms - Swift SDK

On this page

  • Prerequisites
  • About the Examples on This Page
  • Open an Actor-Isolated Realm
  • Define a Custom Realm Actor
  • Use a Realm Actor Synchronously in an Isolated Function
  • Use a Realm Actor in Async Functions
  • Write to an Actor-Isolated Realm
  • Observe Notifications on an Actor-Isolated Realm
  • Register a Collection Change Listener
  • Register an Object Change Listener

Starting with Realm Swift SDK version 10.39.0, Realm supports actor-isolated realm instances. You might want to use an actor-isolated realm if your app uses Swift concurrency language features. This functionality provides an alternative to managing threads or dispatch queues to perform asynchronous work.

With an actor-isolated realm, you can use Swift's async/await syntax to:

  • Open a realm

  • Write to a realm

  • Listen for notifications

For general information about Swift actors, refer to Apple's Actor documentation.

To use Realm in a Swift actor, your project must:

  • Use Realm Swift SDK version 10.39.0 or later

  • Use Swift 5.8/Xcode 14.3

In addition, we strongly recommend enabling these settings in your project:

  • SWIFT_STRICT_CONCURRENCY=complete: enables strict concurrency checking

  • OTHER_SWIFT_FLAGS=-Xfrontend-enable-actor-data-race-checks: enables runtime actor data-race detection

The examples on this page use the following model:

class Todo: Object {
@Persisted(primaryKey: true) var _id: ObjectId
@Persisted var name: String
@Persisted var owner: String
@Persisted var status: String
}

You can use the Swift async/await syntax to await opening a realm.

Initializing a realm with try await Realm() opens a MainActor-isolated realm. Alternately, you can explicitly specify an actor when opening a realm with the await syntax.

@MainActor
func mainThreadFunction() async throws {
// These are identical: the async init produces a
// MainActor-isolated Realm if no actor is supplied
let realm1 = try await Realm()
let realm2 = try await Realm(actor: MainActor.shared)
try await useTheRealm(realm: realm1)
}

You can specify a default configuration or customize your configuration when opening an actor-isolated realm:

@MainActor
func mainThreadFunction() async throws {
let username = "Galadriel"
// Customize the default realm config
var config = Realm.Configuration.defaultConfiguration
config.fileURL!.deleteLastPathComponent()
config.fileURL!.appendPathComponent(username)
config.fileURL!.appendPathExtension("realm")
// Open an actor-isolated realm with a specific configuration
let realm = try await Realm(configuration: config, actor: MainActor.shared)
try await useTheRealm(realm: realm)
}

For more general information about configuring a realm, refer to Configure & Open a Realm - Swift SDK.

You can open a synced realm as an actor-isolated realm:

@MainActor
func mainThreadFunction() async throws {
// Initialize the app client and authenticate a user
let app = App(id: APPID)
let user = try await app.login(credentials: Credentials.anonymous)
// Configure the synced realm
var flexSyncConfig = user.flexibleSyncConfiguration(initialSubscriptions: { subs in
subs.append(QuerySubscription<Todo>(name: "all_todos"))})
flexSyncConfig.objectTypes = [Todo.self]
// Open and use the synced realm
let realm = try await Realm(configuration: flexSyncConfig, actor: MainActor.shared, downloadBeforeOpen: .always)
try await useTheSyncedRealm(realm: realm)
}

For more general information about opening a synced realm, refer to Configure & Open a Synced Realm - Swift SDK.

You can define a specific actor to manage Realm in asynchronous contexts. You can use this actor to manage realm access, perform write operations, and get notifications for changes.

actor RealmActor {
// An implicitly-unwrapped optional is used here to let us pass `self` to
// `Realm(actor:)` within `init`
var realm: Realm!
init() async throws {
realm = try await Realm(actor: self)
}
var count: Int {
realm.objects(Todo.self).count
}
func createTodo(name: String, owner: String, status: String) async throws {
try await realm.asyncWrite {
realm.create(Todo.self, value: [
"_id": ObjectId.generate(),
"name": name,
"owner": owner,
"status": status
])
}
}
func updateTodo(_id: ObjectId, name: String, owner: String, status: String) async throws {
try await realm.asyncWrite {
realm.create(Todo.self, value: [
"_id": _id,
"name": name,
"owner": owner,
"status": status
], update: .modified)
}
}
func deleteTodo(todo: Todo) async throws {
try await realm.asyncWrite {
realm.delete(todo)
}
}
func close() {
realm = nil
}
}

An actor-isolated realm may be used with either local or global actors.

// A simple example of a custom global actor
@globalActor actor BackgroundActor: GlobalActor {
static var shared = BackgroundActor()
}
@BackgroundActor
func backgroundThreadFunction() async throws {
// Explicitly specifying the actor is required for anything that is not MainActor
let realm = try await Realm(actor: BackgroundActor.shared)
try await realm.asyncWrite {
_ = realm.create(Todo.self, value: [
"name": "Pledge fealty and service to Gondor",
"owner": "Pippin",
"status": "In Progress"
])
}
// Thread-confined Realms would sometimes throw an exception here, as we
// may end up on a different thread after an `await`
let todoCount = realm.objects(Todo.self).count
print("The number of Realm objects is: \(todoCount)")
}
@MainActor
func mainThreadFunction() async throws {
try await backgroundThreadFunction()
}

When a function is confined to a specific actor, you can use the actor-isolated realm synchronously.

func createObject(in actor: isolated RealmActor) async throws {
// Because this function is isolated to this actor, you can use
// realm synchronously in this context without async/await keywords
try actor.realm.write {
actor.realm.create(Todo.self, value: [
"name": "Keep it secret",
"owner": "Frodo",
"status": "In Progress"
])
}
let taskCount = actor.count
print("The actor currently has \(taskCount) tasks")
}
let actor = try await RealmActor()
try await createObject(in: actor)

When a function isn't confined to a specific actor, you can use your Realm actor with Swift's async/await syntax.

func createObject() async throws {
// Because this function is not isolated to this actor,
// you must await operations completed on the actor
try await actor.createTodo(name: "Take the ring to Mount Doom", owner: "Frodo", status: "In Progress")
let taskCount = await actor.count
print("The actor currently has \(taskCount) tasks")
}
let actor = try await RealmActor()
try await createObject()

Actor-isolated realms can use Swift async/await syntax for asynchronous writes. Using try await realm.writeAsync { ... } suspends the current task, acquires the write lock without blocking the current thread, and then invokes the block. Realm writes the data to disk on a background thread and resumes the task when that completes.

This function from the example RealmActor defined above shows how you might write to an actor-isolated realm:

func createTodo(name: String, owner: String, status: String) async throws {
try await realm.asyncWrite {
realm.create(Todo.self, value: [
"_id": ObjectId.generate(),
"name": name,
"owner": owner,
"status": status
])
}
}

And you might perform this write using Swift's async syntax:

func createObject() async throws {
// Because this function is not isolated to this actor,
// you must await operations completed on the actor
try await actor.createTodo(name: "Take the ring to Mount Doom", owner: "Frodo", status: "In Progress")
let taskCount = await actor.count
print("The actor currently has \(taskCount) tasks")
}
let actor = try await RealmActor()
try await createObject()

This does not block the calling thread while waiting to write. It does not perform I/O on the calling thread. For small writes, this is safe to use from @MainActor functions without blocking the UI. Writes that negatively impact your app's performance due to complexity and/or platform resource constraints may still benefit from being done on a background thread.

Asynchronous writes are only supported for actor-isolated Realms or in @MainActor functions.

You can observe notifications on an actor-isolated realm using Swift's async/await syntax.

Calling await object.observe() or await collection.observe registers a block to be called each time the object or collection changes.

The SDK asynchronously calls the block on the given actor's executor.

For write transactions performed on different threads or in different processes, the SDK calls the block when the realm is (auto)refreshed to a version including the changes. For local writes, the SDK calls the block at some point in the future after the write transaction is committed.

Like non-actor-confined notifications, you can only observe objects or collections managed by a realm. You must retain the returned token for as long as you want to watch for updates.

If you need to manually advance the state of an observed realm on the main thread or an actor-isolated realm, call await realm.asyncRefresh(). This updates the realm and outstanding objects managed by the Realm to point to the most recent data and deliver any applicable notifications.

Warning

You cannot call the observe method during a write transaction or when the containing realm is read-only.

The SDK calls a collection notification block after each write transaction which:

  • Deletes an object from the collection.

  • Inserts an object into the collection.

  • Modifies any of the managed properties of an object in the collection. This includes self-assignments that set a property to its existing value.

Important

Order Matters

In collection notification handlers, always apply changes in the following order: deletions, insertions, then modifications. Handling insertions before deletions may result in unexpected behavior.

These notifications provide information about the actor on which the change occurred. Like non-actor-isolated collection notifications, they also provide a change parameter that reports which objects are deleted, added, or modified during the write transaction. This RealmCollectionChange resolves to an array of index paths that you can pass to a UITableView's batch update methods.

let actor = try await RealmActor()
// Add a todo to the realm so the collection has something to observe
try await actor.createTodo(name: "Arrive safely in Bree", owner: "Merry", status: "In Progress")
let todoCount = await actor.count
print("The actor currently has \(todoCount) tasks")
// Get a collection
let todos = await actor.realm.objects(Todo.self)
// Register a notification token, providing the actor
let token = await todos.observe(on: actor, { actor, changes in
print("A change occurred on actor: \(actor)")
switch changes {
case .initial:
print("The initial value of the changed object was: \(changes)")
case .update(_, let deletions, let insertions, let modifications):
if !deletions.isEmpty {
print("An object was deleted: \(changes)")
} else if !insertions.isEmpty {
print("An object was inserted: \(changes)")
} else if !modifications.isEmpty {
print("An object was modified: \(changes)")
}
case .error(let error):
print("An error occurred: \(error.localizedDescription)")
}
})
// Make an update to an object to trigger the notification
await actor.realm.writeAsync {
todos.first!.status = "Completed"
}
// Invalidate the token when done observing
token.invalidate()

The SDK calls an object notification block after each write transaction which:

  • Deletes the object.

  • Modifies any of the managed properties of the object. This includes self-assignments that set a property to its existing value.

The block is passed a copy of the object isolated to the requested actor, along with information about what changed. This object can be safely used on that actor.

By default, only direct changes to the object's properties produce notifications. Changes to linked objects do not produce notifications. If a non-nil, non-empty keypath array is passed in, only changes to the properties identified by those keypaths produce change notifications. The keypaths may traverse link properties to receive information about changes to linked objects.

let actor = try await RealmActor()
// Add a todo to the realm so we can observe it
try await actor.createTodo(name: "Scour the Shire", owner: "Merry", status: "In Progress")
let todoCount = await actor.count
print("The actor currently has \(todoCount) tasks")
// Get an object
let todo = await actor.realm.objects(Todo.self).where {
$0.name == "Scour the Shire"
}.first!
// Register a notification token, providing the actor
let token = await todo.observe(on: actor, { actor, change in
print("A change occurred on actor: \(actor)")
switch change {
case .change(let object, let properties):
for property in properties {
print("Property '\(property.name)' of object \(object) changed to '\(property.newValue!)'")
}
case .error(let error):
print("An error occurred: \(error)")
case .deleted:
print("The object was deleted.")
}
})
// Make an update to an object to trigger the notification
await actor.realm.writeAsync {
todo.status = "Completed"
}
// Invalidate the token when done observing
token.invalidate()
←  Use Realm with SwiftUI PreviewsSwift Concurrency - Swift SDK →
Share Feedback
© 2023 MongoDB, Inc.

About

  • Careers
  • Investor Relations
  • Legal Notices
  • Privacy Notices
  • Security Information
  • Trust Center
© 2023 MongoDB, Inc.