Use o Realm com atores - Swift SDK
Nesta página
- Pré-requisitos
- Sobre os exemplos nesta página
- Abra um Realm isolado por ator
- Defina um ator de Realm personalizado
- Utilize um ator de Realm de forma síncrona em uma função isolada
- Utilize um ator de Realm em funções assíncronas
- Grave para um Realm isolado por ator
- Passe dados do Realm além do limite do ator
- Passe uma ThreadSafeReference
- Passe um tipo enviável
- Observe notificações em outro ator
- Limitações de observação
- Registrar um ouvinte de alteração de coleção
- Registrar um ouvinte de alteração de objeto
A partir da versão 10.39.0 do Realm Swift SDK, o Realm oferece suporte à funcionalidade integrada de usar o Realm com atores Swift. O suporte para atores do Realm oferece uma alternativa ao gerenciamento de threads ou filas de despacho para realizar trabalhos assíncronos. Você pode usar o Realm com atores de algumas maneiras:
Trabalhar com o Realm somente em um ator específico com um Realm isolado por ator
Usar o Realm entre atores com base nas necessidades do seu aplicativo
Recomendamos usar um Realm isolado por ator se deseja restringir todo o acesso ao Realm a um único ator. Isso elimina a necessidade de passar dados além dos limites do ator, e pode simplificar a depuração da corrida de dados.
Recomendamos usar Realms entre atores caso queira realizar diferentes tipos de trabalhos em diferentes atores. Por exemplo, você pode ler objetos no MainActor, mas usar um ator de em segundo plano para fazer grandes gravações.
Para obter informações gerais sobre os atores do Swift , consulte a documentação do Actor da Apple .
Pré-requisitos
Para usar o Realm em um ator Swift, seu projeto deve:
Usar o Realm Swift SDK versão 10.39.0 ou posterior
Usar o Swift 5.8/Xcode 14.3
Além disso, é altamente recomendável ativar estas configurações em seu projeto:
SWIFT_STRICT_CONCURRENCY=complete
: habilita uma verificação rigorosa de simultaneidadeOTHER_SWIFT_FLAGS=-Xfrontend-enable-actor-data-race-checks
: habilita a detecção da corrida de dados de ator de runtime
Sobre os exemplos nesta página
Os exemplos nesta página usam o seguinte modelo:
class Todo: Object { true) var _id: ObjectId (primaryKey: var name: String var owner: String var status: String }
Abra um Realm isolado por ator
Você pode usar a sintaxe Swift async/await para aguardar a abertura de um Realm.
Inicializar um Realm com try await Realm()
abre um Realm isolado por MainActor. Como alternativa, você pode especificar explicitamente um ator ao abrir um Realm com a sintaxe 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) }
Você pode especificar uma configuração padrão ou personalizar sua configuração ao abrir um Realm isolado por ator:
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 obter informações gerais sobre como configurar um Realm, consulte Configurar e abrir um Realm - Swift SDK.
Você pode abrir um Realm sincronizado como um Realm isolado por ator:
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 obter informações gerais sobre como abrir um Realm sincronizado, consulte Configurar e abrir um Realm sincronizado - Swift SDK.
Defina um ator de Realm personalizado
Você pode definir um ator específico para gerenciar o Realm em contextos assíncronos. Você pode usar esse ator para gerenciar o acesso ao Realm e executar operações de gravação.
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 } }
Um realm isolado de ator pode ser usado com atores locais ou globais.
// 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() }
Utilize um ator de Realm de forma síncrona em uma função isolada
Quando uma função está confinada a um ator específico, você pode usar o Realm isolado por ator de forma síncrona.
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)
Utilize um ator de Realm em funções assíncronas
Quando uma função não está confinada a um ator específico, você pode usar seu ator de Realm com a sintaxe Swift async/await.
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()
Grave para um Realm isolado por ator
Os domínios isolados por ator podem usar a sintaxe Swift async/await para gravações assíncronas. O uso do try await realm.asyncWrite { ... }
suspende a tarefa atual, adquire o bloqueio de gravação sem bloquear a thread atual e invoca o bloco. O Realm grava os dados no disco em uma thread em segundo plano e retoma a tarefa quando isso for concluído.
Esta função do exemplo RealmActor
definido acima mostra como você pode gravar em um Realm isolado por ator:
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 ]) } }
E você pode fazer essa escrita usando a sintaxe assíncrona do 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()
Isso não bloqueia a thread de chamada enquanto aguarda para gravar. Ele não executa a entrada/saída na thread de chamada. Para pequenas gravações, é seguro usar a partir das funções do @MainActor
sem bloquear a IU. Gravações que afetam negativamente o desempenho do aplicativo devido à complexidade e/ou às restrições de recursos da plataforma ainda podem ser feitas em uma thread em segundo plano.
Gravações assíncronas são aceitas somente em Realms isolados por ator ou em funções @MainActor
.
Passe dados do Realm além do limite do ator
Objetos de Realm não são enviáveis e não podem cruzar o limite do ator diretamente. Para passar dados do Realm além do limite do ator, existem duas opções:
Passe um
ThreadSafeReference
para ou a partir do atorPasse outros tipos que são enviáveis, como passar valores diretamente ou criar structs para passar além do limite do ator
Passe uma ThreadSafeReference
Você pode criar um ThreadSafeReference em um ator onde você tem acesso ao objeto. Neste caso, criamos um ThreadSafeReference
no MainActor
. Em seguida, passe o ThreadSafeReference
para o ator 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)
No ator de destino, você deve resolve()
a referência dentro de uma transação de gravação antes de usá-lo. Isso recupera uma versão do objeto local para aquele ator.
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
Você deve resolver um ThreadSafeReference
exatamente uma vez. Caso contrário, o domínio de origem permanece fixado até que a referência seja desalocada. Por essa razão, ThreadSafeReference
deve ser de curta duração.
Se precisar compartilhar o mesmo objeto de Realm entre atores mais de uma vez, recomendamos compartilhar a chave primária e fazer uma consulta dela no ator onde você deseja usá-la. Consulte a seção "Passar uma chave primária e fazer consulta do objeto em outro ator" nesta página para ver um exemplo.
Passe um tipo enviável
Embora os objetos de Realm não sejam enviáveis, você pode contornar isso passando tipos enviáveis além dos limites do ator. Você pode usar diferentes estratégias para passar tipos enviáveis e trabalhar com dados além dos limites dos atores:
Passar tipos de Realm enviáveis ou valores primitivos em vez de objetos de Realm completos
Passar a chave primária de um objeto e fazer consulta do objeto em outro ator
Criar uma representação enviável do seu objeto de Realm, como uma struct
Passar tipos enviáveis de Realm e valores primitivos
Se você só precisa de uma informação do objeto de Realm, como uma String
ou Int
, você pode passar o valor diretamente entre os atores em vez de passar o objeto de Realm. Para obter uma lista completa de quais tipos de Realm são enviáveis, consulte Tipos enviáveis, não enviáveis e confinados por thread.
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") }
Passar uma chave primária e fazer consulta do objeto em outro ator
Para usar um objeto de Realm em outro ator, você pode compartilhar a chave primária e fazer consulta no ator onde você deseja usá-la.
// 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) }
Criar uma representação enviável do seu objeto
Se precisar trabalhar com mais do que um valor simples, mas não quiser a sobrecarga de passar ThreadSafeReferences
ou fazer consulta de objetos em diferentes atores, você pode criar uma struct ou outra representação enviável de seus dados para passar além do limite do ator.
Por exemplo, o ator pode ter uma função que cria uma representação struct do objeto de 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) }
Em seguida, você pode chamar uma função para obter os dados como uma struct em outro ator.
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 notificações em outro ator
Você pode observar notificações em um Realm isolado por ator usando a sintaxe Swift async/await.
Chamar await object.observe(on: Actor)
ou await collection.observe(on: Actor)
registra um bloqueio a ser chamado sempre que o objeto ou coleção for alterado.
O SDK chama o bloco de forma assíncrona no executor do determinado ator.
Para transações de gravação realizadas em threads ou processos diferentes, o SDK chama o bloco quando o Realm é (auto)atualizado para uma versão que inclui as alterações. Para gravações locais, o SDK chama o bloco em algum momento no futuro depois que a transação de gravação é confirmada.
Assim como outras notificações do Realm, você pode observar somente objetos ou coleções gerenciados por um Realm. Você deve reter o token retornado pelo tempo necessário que deseja receber atualizações.
Se precisar avançar manualmente o estado de um Realm observado na thread principal ou em outro ator, chame await realm.asyncRefresh()
. Isso atualiza o Realm e os objetos pendentes gerenciados pelo Realm para apontar para os dados mais recentes e fornecer quaisquer notificações aplicáveis.
Limitações de observação
Você não pode chamar o método .observe()
:
Durante uma transação de gravação
Quando o Realm contido é somente para leitura
Em um Realm confinado pelo ator de fora do ator
Registrar um ouvinte de alteração de coleção
O SDK chama um bloco de notificação de coleção após cada transação de gravação que:
Exclui um objeto da coleção.
Insere um objeto na coleção.
Modifica qualquer uma das propriedades gerenciadas de um objeto na coleção. Isso inclui autoatribuições que definem uma propriedade com seu valor existente.
Importante
Questões importantes
Em manipuladores de notificações de collection, sempre execute alterações na seguinte ordem: exclusões, inserções e modificações. Manipular inserções antes de exclusões pode resultar em comportamento inesperado.
Essas notificações fornecem informações sobre o ator no qual ocorreu a alteração. Como as notificações de coleção isoladas por não atores , elas também fornecem um parâmetro change
que informa quais objetos foram excluídos, adicionados ou modificados durante a transação de gravação. Esta alteração de coleção de realm soluciona uma array de caminhos de índice que você pode transmitir para os métodos de atualização em lote do 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 um ouvinte de alteração de objeto
O SDK chama um bloco de notificação de objeto após cada transação de gravação que:
Exclui o objeto.
Modifica qualquer uma das propriedades gerenciadas do objeto. Isso inclui autoatribuições que definem uma propriedade com seu valor existente.
O bloco recebe uma cópia do objeto isolado para o ator solicitado, junto com informações sobre o que foi alterado. Esse objeto pode ser usado com segurança nesse ator.
Por padrão, somente alterações diretas nas propriedades do objeto produzem notificações. Alterações em objetos vinculados não produzem notificações. Se uma array de caminho de chave não nulo e não vazio for passado, somente as alterações nas propriedades identificadas por esses caminhos de chave produzirão notificações de alteração. Os caminhos de chave podem atravessar propriedades de link para receber informações sobre alterações em 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() }