Requisitos previos
Tener Xcode 12.4 o posterior (versión mínima de Swift 5.3.1).
Cree un nuevo proyecto Xcode utilizando la plantilla "Aplicación" de SwiftUI con un objetivo iOS mínimo de 15.0.
Instala el SDK de Swift. Esta aplicación SwiftUI requiere una versión mínima del SDK.10.19.0
Overview
Tip
Utilice Realm con SwiftUI
Esta página ofrece una pequeña aplicación práctica para que puedas empezar a usar Realm y SwiftUI rápidamente. Si quieres ver más ejemplos, incluyendo más explicaciones sobre las funciones de SwiftUI de Realm, consulta: SwiftUI - Swift SDK.
Esta página contiene todo el código para una aplicación de Realm y SwiftUI funcional. La aplicación se inicia en el ItemsView, donde puedes editar una lista de elementos:
Presione el botón
Adden la parte inferior derecha de la pantalla para agregar elementos generados aleatoriamente.Presione el botón
Editen la parte superior derecha para modificar el orden de la lista, que la aplicación conserva en el reino.También puedes deslizar para eliminar elementos.
Cuando tengas elementos en la lista, puedes presionar uno de los elementos para navegar al ItemDetailsView. Aquí es donde puedes modificar el nombre del elemento o marcarlo como favorito:
Presiona el campo de texto en el centro de la pantalla y escribe un nuevo nombre. Al presionar Retorno, el nombre del elemento debería actualizarse en toda la aplicación.
También puedes alternar su estado favorito presionando el botón con forma de corazón en la parte superior derecha.
Tip
Esta guía se integra opcionalmente con Sincronización de dispositivos.Consulte Integrar sincronización de dispositivos Atlas a continuación.
Empezar
Suponemos que ha creado un proyecto de Xcode con la plantilla "App" de SwiftUI. Abra el archivo principal de Swift y elimine todo el código, incluyendo las clases @main App que Xcode generó. En la parte superior del archivo, importe los frameworks Realm y SwiftUI:
import RealmSwift import SwiftUI
Tip
¿Quieres profundizar en el código completo? Ve al código completo a continuación.
Definir modelos
Un caso de uso común del modelado de datos de Realm es tener "objetos" y "contenedores de objetos". Esta aplicación define dos modelos de objetos de Realm relacionados: elemento y grupo de elementos.
Un elemento tiene dos propiedades orientadas al usuario:
Un nombre generado aleatoriamente, que el usuario puede editar.
Una propiedad booleana
isFavorite, que muestra si el usuario marcó el elemento como favorito.
Un grupo de elementos contiene elementos. Puedes ampliar el grupo de elementos para que tenga un nombre y una asociación con un usuario específico, pero esto queda fuera del alcance de esta guía.
Pegue el siguiente código en su archivo Swift principal para definir los modelos:
Dado que la sincronización flexible no incluye automáticamente los objetos vinculados, debemos agregar ownerId a ambos objetos. Puede omitir ownerId si solo desea usar un dominio 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. (primaryKey: true) var _id: ObjectId /// 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. (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. 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. (primaryKey: true) var _id: ObjectId /// 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 = "" }
Vistas y objetos observados
El punto de entrada de la aplicación es la clase ContentView que deriva de SwiftUI.App. Por ahora, esto siempre muestra el LocalOnlyContentView. Más adelante, esto mostrará el SyncContentView cuando Device Sync esté activado.
/// 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() } } }
Tip
Puede utilizar un reino distinto del reino predeterminado pasando un objeto de entorno desde un nivel superior en la jerarquía de la vista:
LocalOnlyContentView() .environment(\.realmConfiguration, Realm.Configuration( /* ... */ ))
LocalOnlyContentView tiene un itemGroups con la etiqueta @ObservedResults. Esto utiliza implícitamente el dominio predeterminado para cargar todos los itemGroups al abrir la vista.
该 aplicación 仅期望只存在一个 itemGroup。 如果 realm 中存在 itemGroup,LocalOnlyContentView 将为该 itemGroup renderizar 一个 ItemsView 。
Si no hay ningún itemGroup en el dominio, LocalOnlyContentView muestra un ProgressView mientras agrega uno. Dado que la vista observa los itemGroups gracias al contenedor de propiedades @ObservedResults, se actualiza inmediatamente al agregar el primer itemGroup y muestra el 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()) } } } }
Tip
A partir de la versión 10.12.0 del SDK, puede usar un parámetro de ruta de clave opcional con @ObservedResults para filtrar las notificaciones de cambios únicamente a las que se producen en la ruta o rutas de clave proporcionadas. Por ejemplo:
@ObservedResults(MyObject.self, keyPaths: ["myList.property"])
ItemsView recibe el itemGroup de la vista principal y lo almacena en la propiedad @ObservedRealmObject. Esto permite que ItemsView sepa cuándo el objeto ha cambiado, independientemente de dónde se haya producido.
ItemsView itera sobre los elementos del itemGroup y pasa cada elemento a un ItemRow para representarlo como una lista.
Para definir qué sucede cuando un usuario elimina o mueve una fila, pasamos los remove move métodos y de la Lista de Dominios como controladores de los eventos de eliminación y movimiento de la Lista de SwiftUI. Gracias al @ObservedRealmObject contenedor de propiedades, podemos usar estos métodos sin abrir explícitamente una transacción de escritura. El contenedor de propiedades abre automáticamente una transacción de escritura cuando es necesario.
/// 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 último, las clases ItemRow y ItemDetailsView utilizan el contenedor de propiedad @ObservedRealmObject con el elemento incluido arriba. Estas clases demuestran algunos ejemplos más de cómo utilizar el contenedor de propiedad para mostrar y actualizar propiedades.
/// 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() } }
Tip
@ObservedRealmObject es un objeto congelado. Si desea modificar las propiedades de un @ObservedRealmObject directamente en una transacción de escritura, debe .thaw() primero.
En este punto, ya tienes todo lo necesario para trabajar con Realm y SwiftUI. Pruébalo y comprueba si todo funciona correctamente. Sigue leyendo para aprender a integrar esta aplicación con Device Sync.
Integrar Atlas Device Sync
Ahora que la aplicación Realm funciona, podemos integrarla opcionalmente con Sincronización de dispositivos. La sincronización te permite ver los cambios que realizas en todos los dispositivos. Antes de añadir la sincronización a esta aplicación, asegúrate de lo siguiente:
Habilitar sincronización del dispositivo.
Elija Sincronización flexible
Especifique un clúster y una base de datos.
Activar el modo de desarrollo.
Utiliza
ownerIdcomo el campo consultable.Habilitar sincronización.
Define las reglas que determinan los permisos que tienen los usuarios al usar Device Sync. En este ejemplo, asignamos un rol predeterminado, que se aplica a cualquier colección que no tenga un rol específico. En este ejemplo, un usuario puede leer y escribir datos donde el
user.iddel usuario conectado coincide con elownerIddel 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 } ] }
Ahora, implemente las actualizaciones de su aplicación.
Tip
La versión Sync de esta aplicación cambia un poco el flujo de la misma. La primera pantalla se convierte en LoginView. Al presionar el Log
in Botón, la aplicación navega a ItemsView, donde verá la lista sincronizada de elementos en un solo itemGroup.
En la parte superior del archivo de origen, inicialice una aplicación Realm opcional con su ID de aplicación:
// 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)
Tip
Puedes cambiar la referencia de la aplicación a nil para volver al modo solo local (sin sincronización de dispositivo).
Actualicemos el ContentView principal para mostrar SyncContentView si la referencia de la aplicación no es 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 SyncContentView a continuación.
SyncContentView observa la instancia de la aplicación Realm. Esta instancia es la interfaz con el backend de App Services, que proporciona la autenticación de usuario necesaria para la sincronización. Al observar la instancia de la aplicación, SyncContentView puede reaccionar cuando un usuario inicia o cierra sesión.
Esta vista tiene dos estados posibles:
Si la aplicación Realm no tiene un usuario conectado actualmente, muestra
LoginView.Si la aplicación tiene un usuario conectado, muestra
OpenSyncedRealmView.
En esta vista, tras confirmar que tenemos un usuario, creamos una flexibleSyncConfiguration() que incluye el parámetro initialSubscriptions . Podemos utilizar este parámetro para suscribirnos a campos consultables. Estas suscripciones iniciales buscan datos que coincidan con las queries y sincronizan esos datos al realm. Si no hay datos que coincidan con las queries, el realm se abre con un estado inicial vacío.
Su aplicación cliente solo puede escribir objetos que coincidan con la consulta de suscripción en un dominio abierto con un flexibleSyncConfiguration. Intentar escribir objetos que no coincidan con la consulta hace que la aplicación realice una escritura compensatoria para deshacer la operación de escritura 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() } } }
En nuestras suscripciones, buscamos objetos ItemGroup y Item donde ownerId coincide con el user.id del usuario conectado. Junto con los permisos que usamos al habilitar la sincronización de dispositivos, esto significa que el usuario solo puede leer y escribir sus propios datos.
Flexible Sync no proporciona acceso automático a los objetos vinculados. Por ello, debemos agregar suscripciones para los objetos ItemGroup y Item; no podemos consultar uno solo y obtener los objetos relacionados.
Desde aquí, pasamos la configuración flexibleSyncConfiguration a OpenSyncedRealmView como realmConfiguration mediante un objeto de entorno. Esta es la vista responsable de abrir un dominio y trabajar con los datos. Sync utiliza esta configuración para buscar datos que deben sincronizarse con el dominio.
OpenSyncedRealmView() .environment(\.realmConfiguration, config)
Una vez iniciada la sesión, abrimos el reino de forma asincrónica con el contenedor de propiedad AsyncOpen.
Como hemos inyectado un flexibleSyncConfiguration() en la vista como un valor de entorno, el contenedor de propiedad utiliza esta configuración para iniciar la sincronización y descargar cualquier dato coincidente antes de abrir el realm. Si no hubiéramos proporcionado una configuración, el contenedor de propiedad crearía un flexibleSyncConfiguration() por defecto para nosotros, y podríamos suscribirnos a queries en .onAppear.
// We've injected a `flexibleSyncConfiguration` as an environment value, // so `@AsyncOpen` here opens a realm using that configuration. (appId: YOUR_APP_SERVICES_APP_ID_HERE, timeout: 4000) var asyncOpen
OpenSyncedRealmView activa la enumeración AsyncOpenState, lo que nos permite mostrar diferentes vistas según el estado. En nuestro ejemplo, mostramos un ProgressView mientras nos conectamos a la aplicación y el dominio se sincroniza. Luego, abrimos el dominio, pasando el itemGroup ItemsViewal, o mostramos un ErrorView si no podemos abrirlo.
Tip
Al abrir un reino sincronizado, utilice el contenedor de propiedad AsyncOpen para descargar siempre los cambios sincronizados antes de abrir el reino, o el contenedor de propiedad AutoOpen para abrir un reino mientras se sincroniza en segundo plano. AsyncOpen requiere que el usuario esté en línea, mientras que AutoOpen abre un reino incluso si el usuario está desconectado.
Esta vista tiene algunos estados diferentes:
Mientras se conecta o espera para iniciar sesión, muestra un
ProgressView.Mientras se descargan cambios en el reino, se muestra un
ProgressViewcon un indicador de progreso.Al abrir el dominio, busque un objeto itemGroup. Si aún no existe, créelo. A continuación, muestre la ItemsView del itemGroup en el dominio. Indique un
LogoutButtonque la ItemsView pueda mostrar en la esquina superior izquierda de la barra de navegación.Si hay un error al cargar el reino, muestra una vista de error que contenga el error.
Al ejecutar la aplicación y ver la interfaz principal, no hay elementos en la vista. Esto se debe a que usamos un inicio de sesión anónimo, por lo que esta es la primera vez que este usuario inicia sesión.
/// 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. (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) } } }
En nuestras suscripciones, buscamos objetos ItemGroup y Item donde ownerId coincide con el user.id del usuario conectado. Junto con los permisos que usamos al crear la aplicación Flexible Sync mencionada anteriormente, esto significa que el usuario solo puede leer y escribir sus propios datos.
Flexible Sync no proporciona acceso automático a los objetos vinculados. Por ello, debemos agregar suscripciones para los objetos ItemGroup y Item; no podemos consultar uno solo y obtener los objetos relacionados.
Teniendo esto en cuenta, también debemos actualizar la vista donde estamos creando un objeto ItemGroup. Debemos establecer ownerId como el user.id del usuario 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)
Y también debemos actualizar ItemsView para agregar ownerId cuando creamos Item objetos:
// 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()
Autenticar usuarios con Atlas App Services
LoginView mantiene un estado para mostrar un indicador de actividad o un error. Utiliza una referencia a la instancia de la aplicación Realm pasada anteriormente para iniciar sesión al hacer clic en el botón Log in anonymously.
Tip
En LoginView, puede implementar la autenticación por correo electrónico/contraseña u otro proveedor de autenticación. Para simplificar, este ejemplo utiliza autenticación anónima.
Una vez completado el inicio de sesión, LoginView no necesita hacer nada más. Dado que la vista principal observa la aplicación Realm, detectará cuándo cambia el estado de autenticación del usuario y decidirá mostrar algo diferente a 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) } } }
El botón Cerrar sesión funciona igual que el botón Ver inicio de sesión, pero cierra la sesión en lugar de iniciarla:
/// 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) } }
Una vez iniciada la sesión, la aplicación sigue el mismo flujo que la versión solo local.
Código completo
Si desea copiar y pegar o examinar el código completo con o sin Device Sync, consulte a continuación.
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. (primaryKey: true) var _id: ObjectId /// 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. (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. (primaryKey: true) var _id: ObjectId /// 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. (primaryKey: true) var _id: ObjectId /// 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. (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. 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. (primaryKey: true) var _id: ObjectId /// 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. (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. 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) } } }