Join us at MongoDB.local London on 7 May to unlock new possibilities for your data. Use WEB50 to save 50%.
Register now >
Docs Menu
Docs Home
/ /
SwiftUI

Realm con SwiftUI QuickStart

  • Have Xcode 12.4 or later (minimum Swift version 5.3.1).

  • Create a new Xcode project using the SwiftUI "App" template with a minimum iOS target of 15.0.

  • Install the Swift SDK. This SwiftUI app requires a minimum SDK version of 10.19.0.

Tip

Use Realm with 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:

  • Pulsa el botón Add en la parte inferior derecha de la pantalla para agregar elementos generados aleatoriamente.

  • Press the Edit button on the top right to modify the list order, which the app persists in the realm.

  • You can also swipe to delete items.

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:

  • Press the text field in the center of the screen and type a new name. When you press Return, the item name should update across the app.

  • También puedes cambiar su estado como favorito al pulsar el botón de corazón en la esquina superior derecha.

Tip

Esta guía se integra opcionalmente con Sincronización de dispositivos.Consulte Integrar sincronización de dispositivos Atlas a continuación.

We assume you have created an Xcode project with the SwiftUI "App" template. Open the main Swift file and delete all of the code inside, including any @main App classes that Xcode generated for you. At the top of the file, import the Realm and SwiftUI frameworks:

import RealmSwift
import SwiftUI

Tip

Just want to dive right in with the complete code? Jump to Complete Code below.

A common Realm data modeling use case is to have "things" and "containers of things". This app defines two related Realm object models: item and itemGroup.

An item has two user-facing properties:

  • A randomly generated-name, which the user can edit.

  • Una propiedad booleana isFavorite, que muestra si el usuario "favoreció" el ítem.

Un itemGroup contiene ítems. Puede extender el itemGroup para que tenga un nombre y una asociación con un usuario específico, pero eso está fuera del alcance de esta guía.

Pegue el siguiente código en su archivo Swift principal para definir los modelos:

Debido a que Flexible Sync no incluye automáticamente objetos vinculados, debemos agregar ownerId a ambos objetos. Puede omitir ownerId si solo desea usar un realm 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 = ""
}

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

You can use a realm other than the default realm by passing an environment object from higher in the View hierarchy:

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 un itemGroup en el realm, LocalOnlyContentView muestra una ProgressView mientras agrega uno. Debido a que la vista observa los itemGroups gracias al contenedor de propiedad @ObservedResults, la vista se actualiza inmediatamente al agregar ese primer itemGroup y muestra 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())
}
}
}
}

Tip

Starting in SDK version 10.12.0, you can use an optional key path parameter with @ObservedResults to filter change notifications to only those occurring on the provided key path or key paths. For example:

@ObservedResults(MyObject.self, keyPaths: ["myList.property"])

La ItemsView recibe el itemGroup de la vista padre y lo almacena en una propiedad @ObservedRealmObject. Esto permite que ItemsView "sepa" cuando el objeto ha cambiado, independientemente de dónde haya ocurrido ese cambio.

The ItemsView iterates over the itemGroup's items and passes each item to an ItemRow for rendering as a list.

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

Tip

@ObservedRealmObject es un objeto congelado. Si desea modificar las propiedades de un @ObservedRealmObject directamente en una transacción de escritura, debe .thaw() primero.

At this point, you have everything you need to work with Realm and SwiftUI. Test it out and see if everything is working as expected. Read on to learn how to integrate this app with Device Sync.

Ahora que tenemos una aplicación Realm funcional, opcionalmente podemos integrarla con Device Sync. Sincronizar te permite ver los cambios realizados en todos tus dispositivos. Antes de que puedas agregar sincronización a esta aplicación, asegúrate de:

  • Create an App Services App.

  • Enable anonymous authentication.

  • Habilitar Device Sync.

    1. Elija Sincronización flexible

    2. Specify a cluster and database.

    3. Activar el modo de desarrollo.

    4. Utiliza ownerId como el campo consultable.

    5. Habilita sincronizar.

  • 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.id del usuario conectado coincide con el ownerId del 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 fuente, inicializa una aplicación de Realm opcional con tu ID de la 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

You can change the app reference to nil to switch back to local-only (non-Device Sync) mode.

Let's update the main ContentView to show the SyncContentView if the app reference is not 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()
}
}
}
}

We define the SyncContentView below.

The SyncContentView observes the Realm app instance. The app instance is the interface to the App Services backend, which provides the user authentication required for Sync. By observing the app instance, the SyncContentView can react when a user logs in or out.

This view has two possible states:

  • If the Realm app does not have a currently logged-in user, show the 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.

Your client application can only write objects that match the subscription query to a realm opened with a flexibleSyncConfiguration. Trying to write objects that don't match the query causes the app to perform a compensating write to undo the illegal write operation.

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

In our subscriptions, we're querying for ItemGroup and Item objects where the ownerId matches the logged-in user's user.id. Together with the permissions we used when we enabled Device Sync above, this means that the user can only read and write their own data.

Flexible Sync does not automatically provide access to linked objects. Because of this, we must add subscriptions for both the ItemGroup and Item objects - we can't just query for one or the other and get the related objects.

From here, we pass the flexibleSyncConfiguration to the OpenSyncedRealmView as a realmConfiguration using an environment object. This is the view responsible for opening a realm and working with the data. Sync uses this configuration to search for data that should sync to the realm.

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.
@AsyncOpen(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.

This view has a few different states:

  • While connecting or waiting for login, show a ProgressView.

  • Mientras se descargan los cambios al realm, muestra un ProgressView con 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 LogoutButton que la ItemsView pueda mostrar en la esquina superior izquierda de la barra de navegación.

  • If there is an error loading the realm, show an error view containing the error.

When you run the app and see the main UI, there are no items in the view. That's because we're using anonymous login, so this is the first time this specific user logs in.

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

In our subscriptions, we're querying for ItemGroup and Item objects where the ownerId matches the logged-in user's user.id. Together with the permissions we used when we created the Flexible Sync app above, this means that the user can only read and write their own data.

Flexible Sync does not automatically provide access to linked objects. Because of this, we must add subscriptions for both the ItemGroup and Item objects - we can't just query for one or the other and get the related objects.

With this in mind, we must also update the view here where we are creating a ItemGroup object. We must set the ownerId as the user.id of the logged-in user.

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)

And we must also update the ItemsView to add ownerId when we create Item objects:

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

The LoginView maintains some state in order to display an activity indicator or error. It uses a reference to the Realm app instance passed in from above to log in when the Log in anonymously button is clicked.

Tip

In the LoginView, you can implement email/password authentication or another authentication provider. For simplicity, this example uses Anonymous authentication.

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

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

Once logged in, the app follows the same flow as the local-only version.

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

Volver

SwiftUI

En esta página