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

Realm com o SwiftUI QuickStart

Nesta página

  • Pré-requisitos
  • Visão geral
  • Começar
  • Definir modelos
  • Visualizações e objetos observados
  • Integrar o Atlas Device Sync
  • Autentique usuários com Atlas App Services
  • Código completo
  • 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.

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.

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.

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.
@Persisted(primaryKey: true) var _id: ObjectId
/// The name of the Item, By default, a random name is generated.
@Persisted var name = "\(randomAdjectives.randomElement()!) \(randomNouns.randomElement()!)"
/// A flag indicating whether the user "favorited" the item.
@Persisted var isFavorite = false
/// Users can enter a description, which is an empty string by default
@Persisted var itemDescription = ""
/// The backlink to the `ItemGroup` this item is a part of.
@Persisted(originProperty: "items") var group: LinkingObjects<ItemGroup>
/// 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.
@Persisted 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.
@Persisted(primaryKey: true) var _id: ObjectId
/// The collection of Items in this group.
@Persisted 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.
@Persisted var ownerId = ""
}

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 {
@State var searchFilter: String = ""
// Implicitly use the default realm's objects(ItemGroup.self)
@ObservedResults(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 {
@ObservedRealmObject 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 {
@ObservedRealmObject 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 {
@ObservedRealmObject 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.

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:

  • Criar um aplicativo do App Services.

  • Habilitar a autenticação anônima.

  • Ativar a sincronização de dispositivos.

    1. Escolher a Flexible Sync

    2. Especificar um cluster e banco de dados.

    3. Ativar o Modo de Desenvolvimento.

    4. Usar ownerId como o campo que pode ser consultado.

    5. 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 ao ownerId 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.
@ObservedObject 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.iddo 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.
@AsyncOpen(appId: YOUR_APP_SERVICES_APP_ID_HERE, timeout: 4000) var asyncOpen

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.
@AsyncOpen(appId: YOUR_APP_SERVICES_APP_ID_HERE, timeout: 4000) var asyncOpen
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.iddo 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()

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.
@State var error: Error?
// Keep track of whether login is in progress.
@State 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 {
@State 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.

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.
@Persisted(primaryKey: true) var _id: ObjectId
/// The name of the Item, By default, a random name is generated.
@Persisted var name = "\(randomAdjectives.randomElement()!) \(randomNouns.randomElement()!)"
/// A flag indicating whether the user "favorited" the item.
@Persisted var isFavorite = false
/// Users can enter a description, which is an empty string by default
@Persisted var itemDescription = ""
/// The backlink to the `ItemGroup` this item is a part of.
@Persisted(originProperty: "items") var group: LinkingObjects<ItemGroup>
}
/// 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.
@Persisted(primaryKey: true) var _id: ObjectId
/// The collection of Items in this group.
@Persisted 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 {
@State var searchFilter: String = ""
// Implicitly use the default realm's objects(ItemGroup.self)
@ObservedResults(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 {
@ObservedRealmObject 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 {
@ObservedRealmObject 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 {
@ObservedRealmObject 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.
@Persisted(primaryKey: true) var _id: ObjectId
/// The name of the Item, By default, a random name is generated.
@Persisted var name = "\(randomAdjectives.randomElement()!) \(randomNouns.randomElement()!)"
/// A flag indicating whether the user "favorited" the item.
@Persisted var isFavorite = false
/// Users can enter a description, which is an empty string by default
@Persisted var itemDescription = ""
/// The backlink to the `ItemGroup` this item is a part of.
@Persisted(originProperty: "items") var group: LinkingObjects<ItemGroup>
/// 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.
@Persisted 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.
@Persisted(primaryKey: true) var _id: ObjectId
/// The collection of Items in this group.
@Persisted 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.
@Persisted 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 {
@State var searchFilter: String = ""
// Implicitly use the default realm's objects(ItemGroup.self)
@ObservedResults(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.
@ObservedObject 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.
@AsyncOpen(appId: YOUR_APP_SERVICES_APP_ID_HERE, timeout: 4000) var asyncOpen
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.
@State var error: Error?
// Keep track of whether login is in progress.
@State 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 {
@State 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 {
@ObservedRealmObject 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 {
@ObservedRealmObject 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 {
@ObservedRealmObject 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)
}
}
}

Voltar

SwiftUI