Starting with Realm Swift SDK version 10.39.0, Realm supports built-in functionality for using Realm with Swift Actors. Realm's actor support provides an alternative to managing threads or dispatch queues to perform asynchronous work. You can use Realm with actors in a few different ways:
Trabajar con el realm únicamente en un actor específico con un realm aislado por el actor
Utiliza Realm en todos los participantes según las necesidades de tu aplicación.
You might want to use an actor-isolated realm if you want to restrict all realm access to a single actor. This negates the need to pass data across the actor boundary, and can simplify data race debugging.
You might want to use realms across actors in cases where you want to perform different types of work on different actors. For example, you might want to read objects on the MainActor but use a background actor for large writes.
Para obtener información general sobre los actores de Swift, consulte Documentación de Actor de Apple.
Requisitos previos
To use Realm in a Swift actor, your project must:
Use Realm Swift SDK version 10.39.0 or later
Usa Swift 5.8/Xcode 14.3
Además, recomendamos encarecidamente habilitar estos ajustes en tu proyecto:
SWIFT_STRICT_CONCURRENCY=complete: enables strict concurrency checkingOTHER_SWIFT_FLAGS=-Xfrontend-enable-actor-data-race-checks: permite la detección de carreras de datos de actores en tiempo de ejecución
About the Examples on This Page
Los ejemplos en esta página usan el siguiente modelo:
class Todo: Object { (primaryKey: true) var _id: ObjectId var name: String var owner: String var status: String }
Abrir un Realm aislado por actor
You can use the Swift async/await syntax to await opening a realm.
Inicializar un realm con try await Realm() abre un realm aislado por MainActor. Alternativamente, puedes especificar explícitamente un actor al abrir un realm con la sintaxis await.
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:
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) }
Para obtener información más general sobre cómo configurar un Realm, consulte Configurar y abrir un Realm - Swift SDK.
Puedes abrir un realm sincronizado como un realm aislado por actor:
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) }
Para obtener información más general sobre cómo abrir un realm sincronizado, consulta Configurar y Abrir un Realm Sincronizado - Swift SDK.
Definir un actor de reino personalizado
You can define a specific actor to manage Realm in asynchronous contexts. You can use this actor to manage realm access and perform write operations.
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 getTodoOwner(forTodoNamed name: String) -> String { let todo = realm.objects(Todo.self).where { $0.name == name }.first! return todo.owner } struct TodoStruct { var id: ObjectId var name, owner, status: String } func getTodoAsStruct(forTodoNamed name: String) -> TodoStruct { let todo = realm.objects(Todo.self).where { $0.name == name }.first! return TodoStruct(id: todo._id, name: todo.name, owner: todo.owner, status: todo.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(id: ObjectId) async throws { try await realm.asyncWrite { let todoToDelete = realm.object(ofType: Todo.self, forPrimaryKey: id) realm.delete(todoToDelete!) } } 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 actor BackgroundActor: GlobalActor { static var shared = 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)") } func mainThreadFunction() async throws { try await backgroundThreadFunction() }
Usa un actor de Realm de forma sincrónica en una función aislada
Cuando una función está confinada a un actor específico, puedes utilizar el ámbito aislado del actor de forma sincrónica.
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)
Use a Realm Actor in Async Functions
Cuando una función no está limitada a un actor específico, puedes usar tu actor Realm con la sintaxis async/await de Swift.
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()
Escribir en un ámbito aislado del actor
Actor-isolated realms can use Swift async/await syntax for asynchronous writes. Using try await realm.asyncWrite { ... } 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.
Esta función del ejemplo RealmActor definido anteriormente muestra cómo se puede escribir en un ámbito aislado del actor:
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 ]) } }
Y podrías realizar esta operación de guardar usando la sintaxis asíncrona de Swift:
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()
Esto no bloquea el hilo que llama mientras espera para guardar. No realiza I/O en el subproceso de llamada. Para escrituras pequeñas, esto es seguro de usar desde funciones @MainActor sin bloquear la Interfaz de Usuario. Las escrituras que afectan negativamente el rendimiento de tu aplicación debido a la complejidad y/o restricciones de recursos de la plataforma aún pueden beneficiarse de ejecutarse en un hilo en segundo plano.
Asynchronous writes are only supported for actor-isolated Realms or in @MainActor functions.
Pass Realm Data Across the Actor Boundary
Realm objects are not Sendable, and cannot cross the actor boundary directly. To pass Realm data across the actor boundary, you have two options:
Pasar un
ThreadSafeReferencehacia o desde el actorPasa otros tipos que son transferibles, como pasar valores directamente o creando estructuras para pasar entre límites de actores
Pass a ThreadSafeReference
You can create a ThreadSafeReference on an actor where you have access to the object. In this case, we create a ThreadSafeReference on the MainActor. Then, pass the ThreadSafeReference to the destination actor.
// We can pass a thread-safe reference to an object to update it on a different actor. let todo = todoCollection.where { $0.name == "Arrive safely in Bree" }.first! let threadSafeReferenceToTodo = ThreadSafeReference(to: todo) try await backgroundActor.deleteTodo(tsrToTodo: threadSafeReferenceToTodo)
En el actor de destino, debes resolve() la referencia dentro de una transacción de escritura antes de poder usarla. Esto recupera una versión del objeto local a ese actor.
actor BackgroundActor { public func deleteTodo(tsrToTodo tsr: ThreadSafeReference<Todo>) throws { let realm = try! Realm() try realm.write { // Resolve the thread safe reference on the Actor where you want to use it. // Then, do something with the object. let todoOnActor = realm.resolve(tsr) realm.delete(todoOnActor!) } } }
Importante
You must resolve a ThreadSafeReference exactly once. Otherwise, the source realm remains pinned until the reference gets deallocated. For this reason, ThreadSafeReference should be short-lived.
Si necesita compartir el mismo objeto de dominio entre actores más de una vez, puede preferir compartir la clave principal y consultarla en el actor donde desea usarla. Consulte la sección "Transferir una clave principal y consultar el objeto en otro actor" en esta página para ver un ejemplo.
Pass a Sendable Type
While Realm objects are not Sendable, you can work around this by passing Sendable types across actor boundaries. You can use a few strategies to pass Sendable types and work with data across actor boundaries:
Pasa tipos Realm transferibles o valores primitivos en vez de objetos Realm completos
Pass an object's primary key and query for the object on another actor
Create a Sendable representation of your Realm object, such as a struct
Pasa tipos Realm Sendable y valores primitivos
If you only need a piece of information from the Realm object, such as a String or Int, you can pass the value directly across actors instead of passing the Realm object. For a full list of which Realm types are Sendable, refer to Sendable, Non-Sendable and Thread-Confined Types.
func mainThreadFunction() async throws { // Create an object in an actor-isolated realm. // Pass primitive data to the actor instead of // creating the object here and passing the object. let actor = try await RealmActor() try await actor.createTodo(name: "Prepare fireworks for birthday party", owner: "Gandalf", status: "In Progress") // Later, get information off the actor-confined realm let todoOwner = await actor.getTodoOwner(forTodoNamed: "Prepare fireworks for birthday party") }
Pasar una clave principal y consultar el objeto en otro actor
If you want to use a Realm object on another actor, you can share the primary key and query for it on the actor where you want to use it.
// Execute code on a specific actor - in this case, the @MainActor func mainThreadFunction() async throws { // Create an object off the main actor func createObject(in actor: isolated BackgroundActor) async throws -> ObjectId { let realm = try await Realm(actor: actor) let newTodo = try await realm.asyncWrite { return realm.create(Todo.self, value: [ "name": "Pledge fealty and service to Gondor", "owner": "Pippin", "status": "In Progress" ]) } // Share the todo's primary key so we can easily query for it on another actor return newTodo._id } // Initialize an actor where you want to perform background work let actor = BackgroundActor() let newTodoId = try await createObject(in: actor) let realm = try await Realm() let todoOnMainActor = realm.object(ofType: Todo.self, forPrimaryKey: newTodoId) }
Create a Sendable Representation of Your Object
Si necesitas trabajar con algo más que un valor simple, pero no deseas la sobrecarga de pasar ThreadSafeReferences o consultar objetos en diferentes actores, puedes crear un struct u otra representación Sendable de tus datos para pasar a través del límite del actor.
Por ejemplo, tu actor podría tener una función que cree una representación de estructura del objeto Realm.
struct TodoStruct { var id: ObjectId var name, owner, status: String } func getTodoAsStruct(forTodoNamed name: String) -> TodoStruct { let todo = realm.objects(Todo.self).where { $0.name == name }.first! return TodoStruct(id: todo._id, name: todo.name, owner: todo.owner, status: todo.status) }
Then, you can call a function to get the data as a struct on another actor.
func mainThreadFunction() async throws { // Create an object in an actor-isolated realm. let actor = try await RealmActor() try await actor.createTodo(name: "Leave the ring on the mantle", owner: "Bilbo", status: "In Progress") // Get information as a struct or other Sendable type. let todoAsStruct = await actor.getTodoAsStruct(forTodoNamed: "Leave the ring on the mantle") }
Observe Notifications on a Different Actor
You can observe notifications on an actor-isolated realm using Swift's async/await syntax.
Al llamar a await object.observe(on: Actor) o await collection.observe(on: Actor) se registra un bloque que se llamará cada vez que el objeto o colección cambie.
The SDK asynchronously calls the block on the given actor's executor.
Para las transacciones de escritura realizadas en diferentes hilos o en diferentes procesos, el SDK llama al bloque cuando el realm se actualiza (auto) a una versión que incluya los cambios. Para las escrituras locales, el SDK llama al bloque en algún momento en el futuro después de que la transacción de escritura sea confirmada.
Como otras notificaciones de Realm, solo puedes observar objetos o colecciones gestionados por un realm. Debe conservar el token devuelto mientras desee mantenerse informado sobre las actualizaciones.
If you need to manually advance the state of an observed realm on the main thread or on another actor, 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.
Observation Limitations
You cannot call the .observe() method:
During a write transaction
When the containing realm is read-only
En un realm confinado a un actor desde fuera del actor
Registrar un oyente de cambios de colección
El SDK llama a un bloque de notificación de colección después de cada transacción de escritura que:
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.
Importante
El orden importa
En los controladores de notificaciones de colecciones, aplique siempre los cambios en el siguiente orden: eliminaciones, inserciones y modificaciones. Gestionar las inserciones antes de las eliminaciones puede provocar un comportamiento inesperado.
Estas notificaciones proporcionan información sobre el actor en el que ocurrió el cambio. Al igual que las notificaciones de colección no aisladas por actor, también proporcionan un parámetro change que informa qué objetos se borran, añaden o modifican durante la transacción de escritura. Esta RealmCollectionChange se resuelve en un arreglo de rutas de índices que puedes pasar a los métodos de actualización por lotes de un UITableView.
// Create a simple actor actor BackgroundActor { public func deleteTodo(tsrToTodo tsr: ThreadSafeReference<Todo>) throws { let realm = try! Realm() try realm.write { // Resolve the thread safe reference on the Actor where you want to use it. // Then, do something with the object. let todoOnActor = realm.resolve(tsr) realm.delete(todoOnActor!) } } } // Execute some code on a different actor - in this case, the MainActor func mainThreadFunction() async throws { let backgroundActor = BackgroundActor() let realm = try! await Realm() // Create a todo item so there is something to observe try await realm.asyncWrite { realm.create(Todo.self, value: [ "_id": ObjectId.generate(), "name": "Arrive safely in Bree", "owner": "Merry", "status": "In Progress" ]) } // Get the collection of todos on the current actor let todoCollection = realm.objects(Todo.self) // Register a notification token, providing the actor where you want to observe changes. // This is only required if you want to observe on a different actor. let token = await todoCollection.observe(on: backgroundActor, { 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)") } }) // Update an object to trigger the notification. // This example triggers a notification that the object is deleted. // We can pass a thread-safe reference to an object to update it on a different actor. let todo = todoCollection.where { $0.name == "Arrive safely in Bree" }.first! let threadSafeReferenceToTodo = ThreadSafeReference(to: todo) try await backgroundActor.deleteTodo(tsrToTodo: threadSafeReferenceToTodo) // Invalidate the token when done observing token.invalidate() }
Registrar un objeto de escucha de cambios
El SDK llama a un bloque de notificación de objetos después de cada transacción de escritura que:
Elimina el objeto.
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.
De forma predeterminada, solo los cambios directos en las propiedades del objeto generan notificaciones. Los cambios en los objetos vinculados no las generan. Si se pasa una matriz de rutas de claves no nula ni vacía, solo los cambios en las propiedades identificadas por dichas rutas generan notificaciones de cambio. Las rutas de claves pueden recorrer las propiedades del vínculo para recibir información sobre los cambios en los objetos vinculados.
// Execute some code on a specific actor - in this case, the MainActor func mainThreadFunction() async throws { // Initialize an instance of another actor // where you want to do background work let backgroundActor = BackgroundActor() // Create a todo item so there is something to observe let realm = try! await Realm() let scourTheShire = try await realm.asyncWrite { return realm.create(Todo.self, value: [ "_id": ObjectId.generate(), "name": "Scour the Shire", "owner": "Merry", "status": "In Progress" ]) } // Register a notification token, providing the actor let token = await scourTheShire.observe(on: backgroundActor, { 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.") } }) // Update the object to trigger the notification. // This triggers a notification that the object's `status` property has been changed. try await realm.asyncWrite { scourTheShire.status = "Complete" } // Invalidate the token when done observing token.invalidate() }