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 ao 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 foi 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 wrapper de propriedade @ObservedRealmObject
, podemos usar esses métodos sem abrir explicitamente uma transação de gravação. O wrapper da propriedade abre automaticamente uma transação de gravação 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 Realm. Se nenhum dado corresponder às consultas, o Realm 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 enumeração 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 Realm está sincronizando. Em seguida, abrimos o Realm, passando o itemGroup
para o ItemsView
, ou mostramos um ErrorView
se não pudermos abrir o Realm.
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.
import RealmSwift import SwiftUI // MARK: Models /// 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: } /// 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>() } extension Item { static let item1 = Item(value: ["name": "fluffy coasters", "isFavorite": false, "ownerId": "previewRealm"]) static let item2 = Item(value: ["name": "sudden cinder block", "isFavorite": true, "ownerId": "previewRealm"]) static let item3 = Item(value: ["name": "classy mouse pad", "isFavorite": false, "ownerId": "previewRealm"]) } extension ItemGroup { static let itemGroup = ItemGroup(value: ["ownerId": "previewRealm"]) static var previewRealm: Realm { var realm: Realm let identifier = "previewRealm" let config = Realm.Configuration(inMemoryIdentifier: identifier) do { realm = try Realm(configuration: config) // Check to see whether the in-memory realm already contains an ItemGroup. // If it does, we'll just return the existing realm. // If it doesn't, we'll add an ItemGroup and append the Items. let realmObjects = realm.objects(ItemGroup.self) if realmObjects.count == 1 { return realm } else { try realm.write { realm.add(itemGroup) itemGroup.items.append(objectsIn: [Item.item1, Item.item2, Item.item3]) } return realm } } catch let error { fatalError("Can't bootstrap item data: \(error.localizedDescription)") } } } // MARK: Views // MARK: Main Views /// 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() } } } /// 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()) } } } } // MARK: Item Views /// 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() } } } } struct ItemsView_Previews: PreviewProvider { static var previews: some View { let realm = ItemGroup.previewRealm let itemGroup = realm.objects(ItemGroup.self) ItemsView(itemGroup: itemGroup.first!) } } /// 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() } } struct ItemDetailsView_Previews: PreviewProvider { static var previews: some View { NavigationView { ItemDetailsView(item: Item.item2) } } }
import RealmSwift import SwiftUI // 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) // MARK: Models /// 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 = "" } extension Item { static let item1 = Item(value: ["name": "fluffy coasters", "isFavorite": false, "ownerId": "previewRealm"]) static let item2 = Item(value: ["name": "sudden cinder block", "isFavorite": true, "ownerId": "previewRealm"]) static let item3 = Item(value: ["name": "classy mouse pad", "isFavorite": false, "ownerId": "previewRealm"]) } extension ItemGroup { static let itemGroup = ItemGroup(value: ["ownerId": "previewRealm"]) static var previewRealm: Realm { var realm: Realm let identifier = "previewRealm" let config = Realm.Configuration(inMemoryIdentifier: identifier) do { realm = try Realm(configuration: config) // Check to see whether the in-memory realm already contains an ItemGroup. // If it does, we'll just return the existing realm. // If it doesn't, we'll add an ItemGroup and append the Items. let realmObjects = realm.objects(ItemGroup.self) if realmObjects.count == 1 { return realm } else { try realm.write { realm.add(itemGroup) itemGroup.items.append(objectsIn: [Item.item1, Item.item2, Item.item3]) } return realm } } catch let error { fatalError("Can't bootstrap item data: \(error.localizedDescription)") } } } // MARK: Views // MARK: Main Views /// 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() } } } } /// 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()) } } } } /// 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() } } } /// 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) } } } struct ErrorView: View { var error: Error var body: some View { VStack { Text("Error opening the realm: \(error.localizedDescription)") } } } // MARK: Authentication Views /// 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) } } } struct LoginView_Previews: PreviewProvider { static var previews: some View { LoginView() } } /// 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) } } // MARK: Item Views /// 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 { let user = app?.currentUser 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. // 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() } } } } struct ItemsView_Previews: PreviewProvider { static var previews: some View { let realm = ItemGroup.previewRealm let itemGroup = realm.objects(ItemGroup.self) ItemsView(itemGroup: itemGroup.first!) } } /// 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() } } struct ItemDetailsView_Previews: PreviewProvider { static var previews: some View { NavigationView { ItemDetailsView(item: Item.item2) } } }