Docs Menu
Docs Home
/ /
Kit de desarrollo de software de Swift

Utiliza Realm con Actors - Swift SDK

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.

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 concurrencia

  • OTHER_SWIFT_FLAGS=-Xfrontend-enable-actor-data-race-checks: permite la detección de carreras de datos de actores en tiempo de ejecución

Los ejemplos de esta página utilizan el siguiente modelo:

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

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.

@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)
}

Puede especificar una configuración predeterminada o personalizar su configuración al abrir un reino aislado del actor:

@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)
}

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:

@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)
}

Para obtener información más general sobre cómo abrir un reino sincronizado, consulte Configurar y abrir un reino sincronizado - Swift SDK.

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
@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()
}

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)

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()

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.

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 ThreadSafeReference hacia o desde el actor

  • Pasar otros tipos que se puedan enviar, como pasar valores directamente o crear estructuras para pasar a través de los límites del actor

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.

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

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.

@MainActor
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")
}

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
@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)
}

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.

@MainActor
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")
}

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.

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

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
@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()
}

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
@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()
}

Volver

Usar Realm con SwiftUI Previews