Menu Docs
Página inicial do Docs
/ /
Atlas Device SDKs
/

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 atores Swift, consulte a documentação de ator da Apple.

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 simultaneidade

  • OTHER_SWIFT_FLAGS=-Xfrontend-enable-actor-data-race-checks: habilita a detecção da corrida de dados de ator de runtime

Os exemplos nesta página usam o seguinte modelo:

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

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.

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

Você pode especificar uma configuração padrão ou personalizar sua configuração ao abrir um Realm isolado por ator:

@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 obter informações mais gerais sobre como configurar um domínio, consulte Configurar e abrir um Realm - Swift SDK.

Você pode abrir um Realm sincronizado como um Realm isolado por ator:

@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 obter informações gerais sobre como abrir um Realm sincronizado, consulte Configurar e abrir um Realm sincronizado - Swift SDK.

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

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)

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

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.

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 ator

  • Passe outros tipos que são enviáveis, como passar valores diretamente ou criar structs para passar além do limite do ator

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.

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

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.

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

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

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.

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

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.

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

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 ator , elas também fornecem um parâmetro change que informa quais objetos foram excluídos, adicionados ou modificados durante a transação de escrita. 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
@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()
}

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

Voltar

Use o Realm com prévias do SwiftUI

Próximo

Simultaneidade do Swift