Página inicial do Docs → Desenvolver aplicações → Atlas Device SDKs
Realm com o SwiftUI QuickStart
Nesta página
Pré-requisitos
Ter o Xcode 12.4 ou posterior (versão mínima Swift 5.3.1).
Crie um novo projeto Xcode usando o modelo "App" da SwiftUI com um requisito mínimo do iOS 15.0.
Instale o Swift SDK. Este aplicativo SwiftUI exige uma versão mínima do SDK de 10.19.0.
Visão geral
Dica
Consulte também: Use o Realm com o SwiftUI
Esta página fornece um pequeno aplicativo de trabalho para que você possa começar a usar a Realm e a SwiftUI rapidamente. Se você quiser ver outros exemplos, incluindo mais explicações sobre os recursos da SwiftUI do Realm, consulte: SwiftUI - Swift SDK.
Esta página contém todo o código de um aplicativo Realm e SwiftUI em funcionamento. O aplicativo é iniciado no ItemsView
, onde você pode editar uma lista de itens:
Pressione o botão
Add
na parte inferior direita da tela para adicionar itens gerados aleatoriamente.Pressione o botão
Edit
no canto superior direito para modificar a ordem da lista, que o aplicativo persiste no domínio.Você também pode deslizar para excluir itens.
Quando você tiver itens na lista, você poderá pressionar um dos itens para navegar até o ItemDetailsView
. É aqui que você pode modificar o nome do item ou marcá-lo como favorito:
Pressione o campo de texto no centro da tela e digite um novo nome. Quando você pressiona Retornar, o nome do item deve ser atualizado em toda a aplicação.
Você também pode alternar seu status favorito pressionando o botão de coração no canto superior direito.
Dica
Opcionalmente, este guia se integra à Device Sync. Consulte Integrar Atlas Device Sync abaixo.
Começar
Presumimos que você criou um projeto Xcode com o modelo "App" do SwiftUI. Abra o arquivo Swift principal e exclua todo o código contido, incluindo quaisquer classes do @main
App
que o Xcode gerou para você. No topo do arquivo, importe as estruturas Realm e SwiftUI:
import RealmSwift import SwiftUI
Dica
Quer apenas mergulhar de cabeça no código completo? Acesse o Código completo abaixo.
Definir modelos
Um caso de uso comum da modelagem de dados do Realm é ter "coisas" e "containers de coisas". Esta aplicação define dois Realm Object Models relacionados: item e itemGrupo.
Um item tem duas propriedades voltadas para o usuário:
Um nome gerado aleatoriamente, que o usuário pode editar.
Uma propriedade booleana do
isFavorite
, que mostra se o usuário "favoritou" o item.
Um itemGroup contém itens. Você pode estender o itemGroup para ter um nome e uma associação com um usuário específico, mas isso está fora do escopo deste guia.
Cole o seguinte código no seu arquivo Swift principal para definir os modelos:
Como a sincronização flexível não inclui automaticamente objetos vinculados, precisamos adicionar ownerId
a ambos os objetos. Você pode omitir ownerId
se quiser usar apenas um domínio local.
/// Random adjectives for more interesting demo item names let randomAdjectives = [ "fluffy", "classy", "bumpy", "bizarre", "wiggly", "quick", "sudden", "acoustic", "smiling", "dispensable", "foreign", "shaky", "purple", "keen", "aberrant", "disastrous", "vague", "squealing", "ad hoc", "sweet" ] /// Random noun for more interesting demo item names let randomNouns = [ "floor", "monitor", "hair tie", "puddle", "hair brush", "bread", "cinder block", "glass", "ring", "twister", "coasters", "fridge", "toe ring", "bracelet", "cabinet", "nail file", "plate", "lace", "cork", "mouse pad" ] /// An individual item. Part of an `ItemGroup`. final class Item: Object, ObjectKeyIdentifiable { /// The unique ID of the Item. `primaryKey: true` declares the /// _id member as the primary key to the realm. true) var _id: ObjectId (primaryKey: /// The name of the Item, By default, a random name is generated. var name = "\(randomAdjectives.randomElement()!) \(randomNouns.randomElement()!)" /// A flag indicating whether the user "favorited" the item. var isFavorite = false /// Users can enter a description, which is an empty string by default var itemDescription = "" /// The backlink to the `ItemGroup` this item is a part of. "items") var group: LinkingObjects<ItemGroup> (originProperty: /// Store the user.id as the ownerId so you can query for the user's objects with Flexible Sync /// Add this to both the `ItemGroup` and the `Item` objects so you can read and write the linked objects. var ownerId = "" } /// Represents a collection of items. final class ItemGroup: Object, ObjectKeyIdentifiable { /// The unique ID of the ItemGroup. `primaryKey: true` declares the /// _id member as the primary key to the realm. true) var _id: ObjectId (primaryKey: /// The collection of Items in this group. var items = RealmSwift.List<Item>() /// Store the user.id as the ownerId so you can query for the user's objects with Flexible Sync /// Add this to both the `ItemGroup` and the `Item` objects so you can read and write the linked objects. var ownerId = "" }
Visualizações e objetos observados
O ponto de entrada do aplicativo é a classe ContentView
que deriva de SwiftUI.App
. No momento, isso sempre exibe o LocalOnlyContentView
. No futuro, mostrará SyncContentView
quando o Device Sync estiver habilitado.
/// The main screen that determines whether to present the SyncContentView or the LocalOnlyContentView. /// For now, it always displays the LocalOnlyContentView. @main struct ContentView: SwiftUI.App { var body: some Scene { WindowGroup { LocalOnlyContentView() } } }
Dica
Você pode usar um realm diferente do realm padrão passando um objeto de ambiente de um nível superior na hierarquia da visualização:
LocalOnlyContentView() .environment(\.realmConfiguration, Realm.Configuration( /* ... */ ))
O LocalOnlyContentView tem um @ObservedResults itemGroups. Isto implicitamente usa o domínio padrão para carregar todos os itemGroups quando a visualização aparecer.
Este aplicativo aceita apenas um itemGroup. Se houver um itemGroup no domínio, o LocalOnlyContentView renderizará um ItemsView
para este itemGroup.
Se ainda não houver nenhum itemGroup no domínio, o LocalOnlyContentView exibirá um ProgressView enquanto ele adiciona um. Como a visualização observa os itemGroups graças ao wrapper da propriedade @ObservedResults
, a visualização é atualizada imediatamente após a adição do primeiro itemGroup e exibe o ItemsView.
/// The main content view if not using Sync. struct LocalOnlyContentView: View { var searchFilter: String = "" // Implicitly use the default realm's objects(ItemGroup.self) ItemGroup.self) var itemGroups ( var body: some View { if let itemGroup = itemGroups.first { // Pass the ItemGroup objects to a view further // down the hierarchy ItemsView(itemGroup: itemGroup) } else { // For this small app, we only want one itemGroup in the realm. // You can expand this app to support multiple itemGroups. // For now, if there is no itemGroup, add one here. ProgressView().onAppear { $itemGroups.append(ItemGroup()) } } } }
Dica
A partir do SDK versão 10.12.0, você pode usar um parâmetro de caminho de chave opcional com @ObservedResults
para filtrar as notificações de alteração somente para aquelas que ocorrem no caminho de chave fornecido ou nos caminhos de chave. Por exemplo:
@ObservedResults(MyObject.self, keyPaths: ["myList.property"])
O ItemsView recebe o itemGroup a partir da vista principal e armazena-o numa propriedade @ObservedRealmObject . Isso permite que o ItemsView "saiba" quando o objeto for alterado, independentemente de onde essa alteração ocorreu.
O ItemsView itera sobre os itens do itemGroup e passa cada item para um ItemRow
para renderização como uma lista.
Para definir o que acontece quando um usuário exclui ou move uma linha, passamos os métodos remove
e move
da Lista de Realm como manipuladores dos respectivos eventos remover e mover da SwiftUI List. Graças ao invólucro de propriedades @ObservedRealmObject
, podemos usar esses métodos sem abrir explicitamente uma transação de escrita. O invólucro da propriedade abre automaticamente uma transação de escrita conforme necessário.
/// The screen containing a list of items in an ItemGroup. Implements functionality for adding, rearranging, /// and deleting items in the ItemGroup. struct ItemsView: View { var itemGroup: ItemGroup /// The button to be displayed on the top left. var leadingBarButton: AnyView? var body: some View { NavigationView { VStack { // The list shows the items in the realm. List { ForEach(itemGroup.items) { item in ItemRow(item: item) }.onDelete(perform: $itemGroup.items.remove) .onMove(perform: $itemGroup.items.move) } .listStyle(GroupedListStyle()) .navigationBarTitle("Items", displayMode: .large) .navigationBarBackButtonHidden(true) .navigationBarItems( leading: self.leadingBarButton, // Edit button on the right to enable rearranging items trailing: EditButton()) // Action bar at bottom contains Add button. HStack { Spacer() Button(action: { // The bound collection automatically // handles write transactions, so we can // append directly to it. $itemGroup.items.append(Item()) }) { Image(systemName: "plus") } }.padding() } } } }
Por fim, as classes ItemRow
e ItemDetailsView
usam o invólucro de propriedade @ObservedRealmObject
com o item passado de cima. Estas classes demonstram mais alguns exemplos de como utilizar o invólucro da propriedade para exibir e atualizar propriedades.
/// Represents an Item in a list. struct ItemRow: View { var item: Item var body: some View { // You can click an item in the list to navigate to an edit details screen. NavigationLink(destination: ItemDetailsView(item: item)) { Text(item.name) if item.isFavorite { // If the user "favorited" the item, display a heart icon Image(systemName: "heart.fill") } } } } /// Represents a screen where you can edit the item's name. struct ItemDetailsView: View { var item: Item var body: some View { VStack(alignment: .leading) { Text("Enter a new name:") // Accept a new name TextField("New name", text: $item.name) .navigationBarTitle(item.name) .navigationBarItems(trailing: Toggle(isOn: $item.isFavorite) { Image(systemName: item.isFavorite ? "heart.fill" : "heart") }) }.padding() } }
Dica
@ObservedRealmObject
é um objeto congelado. Se você deseja modificar as propriedades de um @ObservedRealmObject
diretamente em uma transação por escrito, você deve .thaw()
primeiro.
Neste ponto, você tem tudo de que precisa para trabalhar com Realm e SwiftUI. Teste e veja se tudo está funcionando conforme o esperado. Continue lendo para saber como integrar este aplicativo à sincronização de dispositivos.
Integrar o Atlas Device Sync
Agora que temos um aplicativo Realm em funcionamento, podemos integrá-lo opcionalmente ao Device Sync. A sincronização permite que você veja as alterações feitas em todos os dispositivos. Antes de adicionar a sincronização a esse aplicativo, não deixe de:
Ativar a sincronização de dispositivos.
Escolher a Flexible Sync
Especificar um cluster e banco de dados.
Ativar o Modo de Desenvolvimento.
Usar
ownerId
como o campo que pode ser consultado.Habilite a sincronização.
Defina as regras que determinam quais permissões os usuários terão ao usar o Device Sync. Para este exemplo, atribuímos uma role padrão, que se aplica a qualquer collection que não tenha uma role específica da collection. Neste exemplo, um usuário pode ler e escrever dados onde o
user.id
do usuário conectado corresponde aoownerId
do objeto:{ "roles": [ { "name": "owner-read-write", "apply_when": {}, "document_filters": { "write": { "ownerId": "%%user.id" }, "read": { "ownerId": "%%user.id" } }, "read": true, "write": true, "insert": true, "delete": true, "search": true } ] }
Agora, implante as atualizações do seu aplicativo.
Dica
A versão Sync deste aplicativo muda um pouco o fluxo do aplicativo. A primeira tela se torna o LoginView
. Ao pressionar o botão Log
in, o aplicativo navega até o ItemsView, onde você verá a lista sincronizada de itens em um único ItemGroup.
Na parte superior do arquivo de origem, inicialize um aplicativo Realm opcional com sua ID do aplicativo:
// MARK: Atlas App Services (Optional) // The App Services App. Change YOUR_APP_SERVICES_APP_ID_HERE to your App Services App ID. // If you don't have a App Services App and don't wish to use Sync for now, // you can change this to: // let app: RealmSwift.App? = nil let app: RealmSwift.App? = RealmSwift.App(id: YOUR_APP_SERVICES_APP_ID_HERE)
Dica
Você pode alterar a referência da aplicação para nil
para voltar ao modo somente local (não Sincronização de Dispositivos).
Vamos atualizar o ContentView principal para mostrar o SyncContentView
se a referência do aplicativo não for nil
:
/// The main screen that determines whether to present the SyncContentView or the LocalOnlyContentView. @main struct ContentView: SwiftUI.App { var body: some Scene { WindowGroup { // Using Sync? if let app = app { SyncContentView(app: app) } else { LocalOnlyContentView() } } } }
Definimos o SyncContentView abaixo.
O SyncContentView observa a instância do aplicativo Realm. A instância do aplicativo é a interface para o backend do App Services, que fornece a autenticação do usuário necessária para a sincronização. Observando a instância do aplicativo, o SyncContentView pode reagir quando um usuário faz login ou logout.
Esta visualização tem dois estados possíveis:
Se o aplicativo Realm não tiver um usuário conectado no momento, mostre o
LoginView
.Se o aplicativo tiver um usuário conectado, mostre o
OpenSyncedRealmView
.
Nesta visualização, após confirmar que temos um usuário, criamos uma flexibleSyncConfiguration() que inclui o parâmetro initialSubscriptions
. Podemos utilizar este parâmetro para assinar campos de consulta. Essas assinaturas iniciais procuram dados que correspondam às consultas e sincronizam esses dados com o domínio. Se nenhum dado corresponder às consultas, o domínio será aberto com um estado inicial vazio.
Seu aplicativo cliente só pode gravar objetos que correspondam à query de assinatura em um reino aberto com um flexibleSyncConfiguration
. A tentativa de gravar objetos que não correspondem à query faz com que o aplicativo execute uma gravação compensatória para desfazer a operação de gravação ilegal.
/// This view observes the Realm app object. /// Either direct the user to login, or open a realm /// with a logged-in user. struct SyncContentView: View { // Observe the Realm app object in order to react to login state changes. var app: RealmSwift.App var body: some View { if let user = app.currentUser { // Create a `flexibleSyncConfiguration` with `initialSubscriptions`. // We'll inject this configuration as an environment value to use when opening the realm // in the next view, and the realm will open with these initial subscriptions. let config = user.flexibleSyncConfiguration(initialSubscriptions: { subs in // Check whether the subscription already exists. Adding it more // than once causes an error. if let foundSubscriptions = subs.first(named: "user_groups") { // Existing subscription found - do nothing return } else { // Add queries for any objects you want to use in the app // Linked objects do not automatically get queried, so you // must explicitly query for all linked objects you want to include subs.append(QuerySubscription<ItemGroup>(name: "user_groups") { // Query for objects where the ownerId is equal to the app's current user's id // This means the app's current user can read and write their own data $0.ownerId == user.id }) subs.append(QuerySubscription<Item>(name: "user_items") { $0.ownerId == user.id }) } }) OpenSyncedRealmView() .environment(\.realmConfiguration, config) } else { // If there is no user logged in, show the login view. LoginView() } } }
Em nossas assinaturas, estamos consultando objetos ItemGroup
e Item
em que o ownerId
corresponde ao user.id
do usuário conectado. Juntamente com as permissões que usamos quando ativamos o Device Sync acima, isso significa que o usuário só pode ler e gravar seus próprios dados.
A Sincronização flexível não fornece acesso automático aos objetos vinculados. Por isso, precisamos adicionar assinaturas para os objetos ItemGroup
e Item
. Não podemos simplesmente fazer query de um ou outro e obter os objetos relacionados.
A partir daqui, passamos a flexibleSyncConfiguration para OpenSyncedRealmView como um realmConfiguration
utilizando um objeto de ambiente. Esta é a visão responsável por abrir um domínio e trabalhar com os dados. A Sincronização usa essa configuração para procurar dados que devem ser sincronizados com a região.
OpenSyncedRealmView() .environment(\.realmConfiguration, config)
Uma vez conectado, abrimos o domínio assincronamente com o wrapper da propriedade AsyncOpen .
Como injetamos um flexibleSyncConfiguration()
na visualização como um valor de ambiente, o wrapper de propriedade utiliza esta configuração para iniciar a Sincronização e baixar quaisquer dados correspondentes antes de abrir o domínio. Se não tivéssemos fornecido uma configuração, o wrapper de propriedade criaria um flexibleSyncConfiguration()
padrão para nós e poderíamos assinar em .onAppear
.
// We've injected a `flexibleSyncConfiguration` as an environment value, // so `@AsyncOpen` here opens a realm using that configuration. YOUR_APP_SERVICES_APP_ID_HERE, timeout: 4000) var asyncOpen (appId:
O OpenSyncedRealmView ativa o enum AsyncOpenState, que nos permite mostrar diferentes exibições com base no estado. Em nosso exemplo, mostramos um ProgressView
enquanto nos conectamos ao aplicativo e o domínio está sincronizando. Em seguida, abrimos o domínio, passando o itemGroup
para o ItemsView
, ou mostramos um ErrorView
se não pudermos abrir o domínio.
Dica
Ao abrir um Realm sincronizado, use o wrapper de propriedade AsyncOpen para sempre baixar as alterações sincronizadas antes de abrir o Realm, ou o wrapper de propriedade AutoOpen para abrir um Realm durante a sincronização no background. O AsyncOpen
exige que o usuário esteja online, enquanto o AutoOpen
abre um Realm mesmo se o usuário estiver offline.
Esta visualização tem alguns estados diferentes:
Ao conectar ou aguardar login, mostre um
ProgressView
.Ao baixar alterações no domínio, mostre um
ProgressView
com um indicador de progresso.Quando o domínio for aberto, verifique se há um objeto itemGroup. Se ainda não houver, crie um. Em seguida, mostre o ItemsView para o itemGroup no domínio. Forneça um
LogoutButton
que o ItemsView possa exibir no canto superior esquerdo da barra de navegação.Se houver um erro ao carregar o domínio, mostre uma visualização de erro contendo o erro.
Quando você executa o aplicativo e visualiza a interface do usuário principal, não há itens na visualização. Isso ocorre porque estamos usando login anônimo, então esta é a primeira vez que esse usuário específico faz login.
/// This view opens a synced realm. struct OpenSyncedRealmView: View { // We've injected a `flexibleSyncConfiguration` as an environment value, // so `@AsyncOpen` here opens a realm using that configuration. YOUR_APP_SERVICES_APP_ID_HERE, timeout: 4000) var asyncOpen (appId: var body: some View { // Because we are setting the `ownerId` to the `user.id`, we need // access to the app's current user in this view. let user = app?.currentUser switch asyncOpen { // Starting the Realm.asyncOpen process. // Show a progress view. case .connecting: ProgressView() // Waiting for a user to be logged in before executing // Realm.asyncOpen. case .waitingForUser: ProgressView("Waiting for user to log in...") // The realm has been opened and is ready for use. // Show the content view. case .open(let realm): ItemsView(itemGroup: { if realm.objects(ItemGroup.self).count == 0 { try! realm.write { // Because we're using `ownerId` as the queryable field, we must // set the `ownerId` to equal the `user.id` when creating the object realm.add(ItemGroup(value: ["ownerId":user!.id])) } } return realm.objects(ItemGroup.self).first! }(), leadingBarButton: AnyView(LogoutButton())).environment(\.realm, realm) // The realm is currently being downloaded from the server. // Show a progress view. case .progress(let progress): ProgressView(progress) // Opening the Realm failed. // Show an error view. case .error(let error): ErrorView(error: error) } } }
Em nossas assinaturas, estamos consultando objetos ItemGroup
e Item
em que o ownerId
corresponde ao user.id
do usuário conectado. Juntamente com as permissões que usamos quando criamos o aplicativo Sincronização flexível acima, isso significa que o usuário só pode ler e gravar seus próprios dados.
A Sincronização flexível não fornece acesso automático aos objetos vinculados. Por isso, precisamos adicionar assinaturas para os objetos ItemGroup
e Item
. Não podemos simplesmente fazer query de um ou outro e obter os objetos relacionados.
Com isso em mente, também devemos atualizar a visualização aqui em que estamos criando um objeto ItemGroup
. Devemos definir o ownerId
como user.id
do usuário conectado.
ItemsView(itemGroup: { if realm.objects(ItemGroup.self).count == 0 { try! realm.write { // Because we're using `ownerId` as the queryable field, we must // set the `ownerId` to equal the `user.id` when creating the object realm.add(ItemGroup(value: ["ownerId":user!.id])) } } return realm.objects(ItemGroup.self).first! }(), leadingBarButton: AnyView(LogoutButton())).environment(\.realm, realm)
E também devemos atualizar o ItemsView
para adicionar ownerId
ao criarmos objetos Item
:
// Action bar at bottom contains Add button. HStack { Spacer() Button(action: { // The bound collection automatically // handles write transactions, so we can // append directly to it. // Because we are using Flexible Sync, we must set // the item's ownerId to the current user.id when we create it. $itemGroup.items.append(Item(value: ["ownerId":user!.id])) }) { Image(systemName: "plus") } }.padding()
Autentique usuários com Atlas App Services
O LoginView mantém algum estado para exibir um indicador de atividade ou erro. Utiliza uma referência à instância do aplicativo Realm de cima para iniciar sessão quando o botão Log in anonymously é clicado.
Dica
No LoginView, você pode implementar a e-mail/autenticação de senha ou fornecedor de autenticação. Para simplificar, este exemplo usa autenticação anônima.
Depois que o login for concluído, o LoginView em si não precisará fazer mais nada. Como a visualização principal está observando o aplicativo Realm, ela notará quando o estado de autenticação do usuário for alterado e decidirá mostrar algo diferente do LoginView.
/// Represents the login screen. We will have a button to log in anonymously. struct LoginView: View { // Hold an error if one occurs so we can display it. var error: Error? // Keep track of whether login is in progress. var isLoggingIn = false var body: some View { VStack { if isLoggingIn { ProgressView() } if let error = error { Text("Error: \(error.localizedDescription)") } Button("Log in anonymously") { // Button pressed, so log in isLoggingIn = true Task { do { let user = try await app!.login(credentials: .anonymous) // Other views are observing the app and will detect // that the currentUser has changed. Nothing more to do here. print("Logged in as user with id: \(user.id)") } catch { print("Failed to log in: \(error.localizedDescription)") // Set error to observed property so it can be displayed self.error = error return } } }.disabled(isLoggingIn) } } }
O botão LogoutButton funciona como o LoginView, mas termina a sessão em vez de iniciar a sessão:
/// A button that handles logout requests. struct LogoutButton: View { var isLoggingOut = false var body: some View { Button("Log Out") { guard let user = app!.currentUser else { return } isLoggingOut = true Task { do { try await app!.currentUser!.logOut() // Other views are observing the app and will detect // that the currentUser has changed. Nothing more to do here. } catch { print("Error logging out: \(error.localizedDescription)") } } }.disabled(app!.currentUser == nil || isLoggingOut) } }
Uma vez logado, o aplicativo segue o mesmo fluxo da versão somente local.
Código completo
Se você deseja copiar e colar ou examinar o código completo com ou sem Device Sync, consulte abaixo.