A partir de la versión 10.39.0 del SDK de Realm Swift, Realm incorpora funciones para usar Realm con actores Swift. La compatibilidad con actores de Realm ofrece una alternativa a la gestión de subprocesos o colas de despacho para realizar trabajo asincrónico. Puedes usar Realm con actores de diferentes maneras:
Trabajar con el reino solo en un actor específico con un reino aislado del actor
Utilice Realm entre actores según las necesidades de su aplicación
Se recomienda usar un dominio aislado por actor si se desea restringir el acceso a todo el dominio a un solo actor. Esto elimina la necesidad de pasar datos a través del límite del actor y simplifica la depuración de la carrera de datos.
Podría ser útil usar dominios entre actores si desea realizar diferentes tipos de trabajo en diferentes actores. Por ejemplo, podría querer leer objetos en el actor principal, pero usar un actor en segundo plano para escrituras grandes.
Para obtener información general sobre los actores de Swift, consulte Documentación de Actor de Apple.
Requisitos previos
Para usar Realm en un actor Swift, su proyecto debe:
Utilice la versión 10.39.0 o posterior del SDK de Realm Swift
Utilice Swift 5.8/Xcode 14.3
Además, recomendamos encarecidamente habilitar estas configuraciones en su proyecto:
SWIFT_STRICT_CONCURRENCY=complete: permite una verificación estricta de concurrenciaOTHER_SWIFT_FLAGS=-Xfrontend-enable-actor-data-race-checks: permite la detección de carreras de datos de actores en tiempo de ejecución
Acerca de los ejemplos de esta página
Los ejemplos de esta página utilizan el siguiente modelo:
class Todo: Object { (primaryKey: true) var _id: ObjectId var name: String var owner: String var status: String }
Abrir un reino aislado del actor
Puede utilizar la sintaxis Swift async/await para esperar la apertura de un reino.
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) }
Puede especificar una configuración predeterminada o personalizar su configuración al abrir un reino aislado del actor:
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 reino, consulte Configurar y abrir un reino - 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 reino sincronizado, consulte Configurar y abrir un reino sincronizado - Swift SDK.
Definir un actor de reino personalizado
Puedes definir un actor específico para gestionar el dominio en contextos asíncronos. Puedes usar este actor para gestionar el acceso al dominio y realizar operaciones de escritura.
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 } }
Un reino aislado del actor se puede utilizar con actores locales o globales.
// 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() }
Utilice un actor de reino 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)
Usar un actor de reino en funciones asíncronas
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
Los dominios con aislamiento de actores pueden usar la sintaxis Swift async/await para escrituras asincrónicas. Usar try await realm.asyncWrite { ... } suspende la tarea actual, adquiere el bloqueo de escritura sin bloquear el hilo actual y, a continuación, invoca el bloqueo. El dominio escribe los datos en el disco en un hilo en segundo plano y reanuda la tarea al finalizar.
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 puedes realizar esta escritura utilizando 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.
Las escrituras asincrónicas solo se admiten en reinos aislados del actor o en funciones @MainActor.
Pasar datos del reino a través del límite del actor
Los objetos de dominio no son Enviables y no pueden cruzar el límite del actor directamente. Para pasar datos de dominio a través del límite del actor, tiene dos opciones:
Pasar un
ThreadSafeReferencehacia o desde el actorPasar otros tipos que se puedan enviar, como pasar valores directamente o crear estructuras para pasar a través de los límites del actor
Pasar una ThreadSafeReference
Puedes crear una ThreadSafeReference en un actor con acceso al objeto. En este caso, creamos un ThreadSafeReference en MainActor el. Luego, pasamos el ThreadSafeReference al actor de destino.
// 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, debe resolve() la referencia dentro de una transacción de escritura para poder usarla. Esto recupera una versión del objeto local para 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
Debe resolver un ThreadSafeReference solo una vez. De lo contrario, el dominio de origen permanece anclado hasta que se desasigne la referencia. Por esta razón, ThreadSafeReference debería tener una duración corta.
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.
Pasar un tipo enviable
Aunque los objetos de Realm no son Enviables, se puede solucionar esto pasando tipos Enviables a través de los límites de actores. Se pueden usar algunas estrategias para pasar tipos Enviables y trabajar con datos a través de los límites de actores:
Pase tipos de reino enviables o valores primitivos en lugar de objetos de reino completos
Pase la clave principal de un objeto y consulte el objeto en otro actor
Cree una representación Sendable de su objeto Realm, como una estructura
Pasar tipos de reino enviables y valores primitivos
Si solo necesita información del objeto Realm, como String Into, puede pasar el valor directamente a los actores en lugar de pasar el objeto Realm. Para obtener una lista completa de los tipos de Realm que se pueden enviar, consulte Tipos que se pueden enviar, que no se pueden enviar y que están limitados por subprocesos.
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
Si desea utilizar un objeto Realm en otro actor, puede compartir la clave principal y consultarla en el actor donde desea utilizarla.
// 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) }
Cree una representación enviable de su objeto
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) }
Luego, puedes llamar a una función para obtener los datos como una estructura en otro 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") }
Observar notificaciones en un actor diferente
Puede observar notificaciones en un ámbito aislado del actor utilizando la sintaxis async/await de Swift.
Al llamar a await object.observe(on: Actor) o await collection.observe(on: Actor) se registra un bloque que se llamará cada vez que cambie el objeto o la colección.
El SDK llama asincrónicamente al bloque en el ejecutor del actor dado.
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.
Al igual que otras notificaciones de Realm, solo puedes observar objetos o colecciones administrados por un realm. Debes conservar el token devuelto mientras quieras estar al tanto de las actualizaciones.
Si necesita avanzar manualmente el estado de un dominio observado en el hilo principal o en otro actor, llame a await realm.asyncRefresh(). Esto actualiza el dominio y los objetos pendientes administrados por este para que apunten a los datos más recientes y envíen las notificaciones pertinentes.
Limitaciones de la observación
No puedes llamar al .observe() método:
Durante una transacción de escritura
Cuando el reino contenedor es de sólo lectura
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 recopilación después de cada transacción de escritura que:
Elimina un objeto de la colección.
Inserta un objeto en la colección.
Modifica cualquiera de las propiedades administradas de un objeto de la colección. Esto incluye las autoasignaciones que establecen una propiedad con su valor actual.
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 detector de cambios de objetos
El SDK llama a un bloque de notificación de objetos después de cada transacción de escritura que:
Elimina el objeto.
Modifica cualquiera de las propiedades administradas del objeto. Esto incluye las autoasignaciones que establecen una propiedad a su valor actual.
El bloque recibe una copia del objeto aislado para el actor solicitado, junto con información sobre los cambios. Este objeto puede usarse de forma segura en ese 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() }