Realm 和 SwiftUI 快速入门
先决条件
拥有 Xcode 12.4 或更高版本(最低 Swift 版本为 5.3.1)。
使用 SwiftUI“App”模板创建一个新的 Xcode 项目,且最低 iOS 目标版本为 15.0。
安装 Swift SDK。此 SwiftUI 应用需要的最低 SDK 版本为 10.19.0。
Overview
提示
另请参阅:将 Realm 与 SwiftUI 结合使用
本页提供了一个小型可用应用,以便您快速启动并运行 Realm 和 SwiftUI。如果您想查看其他示例,包括有关 Realm SwiftUI 功能的更多说明,请参阅:SwiftUI - Swift SDK。
本页包含正常工作的 Realm 和 SwiftUI 应用的所有代码。该应用在 ItemsView
上启动,您可以在其中编辑项目列表:
按屏幕右下角的
Add
按钮可添加随机生成的项。按右上角的
Edit
按钮可修改列表顺序,而该应用会在 Realm 中持久保持此顺序。您也可滑动以删除项目。
在列表中具有项目时,您可以按其中的一个项目以导航到 ItemDetailsView
。您可以在此处修改项目名称,或将其标记为常用的项目:
按屏幕中央的文本字段,然后键入新名称。当您按下 Return(返回)时,此项目名称应在整个应用中更新。
您还可通过按右上方的心形开关来切换其最喜爱的状态。
提示
本指南可以选择与 Device Sync集成。 请参阅下面的集成Atlas Device Sync 。
开始体验
我们假定您已使用 SwiftUI“App”模板创建了一个 Xcode 项目。打开 Swift 主文件并删除其中的所有代码,包括 Xcode 为您生成的任何 @main
App
类。在此文件顶部,导入 Realm 和 SwiftUI 框架:
import RealmSwift import SwiftUI
提示
想直接使用完整代码进行操作?跳转到下面的完整代码。
定义模型
常见的 Realm 数据建模用例旨在拥有“内容”和“内容的 container”。此应用定义了两个相关的 Realm 对象模型:item 和 itemGroup。
某一项目具有两个面向用户的属性:
一个随机生成的名称,而用户可对其进行编辑。
一个
isFavorite
布尔型属性,它显示了用户是否已“收藏”此项目。
一个 itemGroup 包含若干项目。您可以扩展 itemGroup 以包含名称以及与特定用户的关联,但这超出了本指南的范围。
将以下代码粘贴到 Swift 主文件以定义这些模型:
由于灵活同步不会自动包含链接的对象,因此必须将 ownerId
添加到这两个对象。如果您只想使用本地 Realm,则可省略 ownerId
。
/// Random adjectives for more interesting demo item names let randomAdjectives = [ "fluffy", "classy", "bumpy", "bizarre", "wiggly", "quick", "sudden", "acoustic", "smiling", "dispensable", "foreign", "shaky", "purple", "keen", "aberrant", "disastrous", "vague", "squealing", "ad hoc", "sweet" ] /// Random noun for more interesting demo item names let randomNouns = [ "floor", "monitor", "hair tie", "puddle", "hair brush", "bread", "cinder block", "glass", "ring", "twister", "coasters", "fridge", "toe ring", "bracelet", "cabinet", "nail file", "plate", "lace", "cork", "mouse pad" ] /// An individual item. Part of an `ItemGroup`. final class Item: Object, ObjectKeyIdentifiable { /// The unique ID of the Item. `primaryKey: true` declares the /// _id member as the primary key to the realm. true) var _id: ObjectId (primaryKey: /// The name of the Item, By default, a random name is generated. var name = "\(randomAdjectives.randomElement()!) \(randomNouns.randomElement()!)" /// A flag indicating whether the user "favorited" the item. var isFavorite = false /// Users can enter a description, which is an empty string by default var itemDescription = "" /// The backlink to the `ItemGroup` this item is a part of. "items") var group: LinkingObjects<ItemGroup> (originProperty: /// Store the user.id as the ownerId so you can query for the user's objects with Flexible Sync /// Add this to both the `ItemGroup` and the `Item` objects so you can read and write the linked objects. var ownerId = "" } /// Represents a collection of items. final class ItemGroup: Object, ObjectKeyIdentifiable { /// The unique ID of the ItemGroup. `primaryKey: true` declares the /// _id member as the primary key to the realm. true) var _id: ObjectId (primaryKey: /// The collection of Items in this group. var items = RealmSwift.List<Item>() /// Store the user.id as the ownerId so you can query for the user's objects with Flexible Sync /// Add this to both the `ItemGroup` and the `Item` objects so you can read and write the linked objects. var ownerId = "" }
视图与观察对象
应用的入口点是从 SwiftUI.App
派生的 ContentView
类。目前,它始终显示 LocalOnlyContentView
。以后,在启用了 Device Sync 时,它显示 SyncContentView
。
/// 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() } } }
提示
您可通过传递位于视图层次结构中较高层的环境对象来使用默认 Realm 以外的 Realm:
LocalOnlyContentView() .environment(\.realmConfiguration, Realm.Configuration( /* ... */ ))
LocalOnlyContentView 具有 @ObservedResults itemGroup。当此视图显示时,此项目会隐式使用默认域来加载所有 itemGroup。
该应用仅要求具有一个 itemGroup。如果在 Realm 中具有一个 itemGroup,则 LocalOnlyContentView 为该 itemGroup 呈现一个 ItemsView
。
如果领域中尚无 itemGroup,LocalOnlyContentView 则会在添加一个 itemGroup 时显示 ProgressView。由于此视图会通过 @ObservedResults
属性包装器来观察 itemGroup,因此该视图在添加第一个 itemGroup 后会立即刷新并显示 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()) } } } }
提示
从 SDK 10.12.0 版本开始,您可将可选关键路径参数与 @ObservedResults
结合使用,从而将变更通知筛选为仅显示在所提供一个或多个关键路径中出现的通知。例如:
@ObservedResults(MyObject.self, keyPaths: ["myList.property"])
ItemsView 从父视图接收 itemGroup 并将其存储在@ObservedRealmObject属性中。 这样,ItemsView 就能“知道”对象何时发生更改,而无论更改发生在何处。
ItemsView 对该 itemGroup 的项目进行遍历,并将每个项目传递给一个 ItemRow
以呈现为列表。
为了定义用户删除或移动行时发生的情况,我们传递Realm列表的remove
和move
方法,作为SwiftUI列表相应删除和移动事件的处理程序。 借助@ObservedRealmObject
属性包装器,我们可以使用这些方法,而无需显式打开写事务(write transaction)。 属性包装器会根据需要自动打开写事务(write transaction)。
/// 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() } } } }
最后,ItemRow
和 ItemDetailsView
类使用 @ObservedRealmObject
属性包装器以处理从上面传入的项目。这些类提供了更多示例,以说明如何使用属性包装器显示和更新属性。
/// 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() } }
提示
@ObservedRealmObject
是一个冻结对象。如果要在写事务中直接修改 @ObservedRealmObject
的属性,则须先对其运行 .thaw()
。
此时,您便已拥有使用 Realm 和 SwiftUI 所需的一切。测试一下,看看一切是否能正常工作。继续阅读以了解如何将此应用与 Device Sync 集成。
集成 Atlas Device Sync
现在,我们已有一个可用 Realm 应用,并可选择与 Device Sync 相集成。Sync 允许您查看在不同设备中所做的更改。在向此应用添加 Device Sync 功能之前,请务必:
指定集群和数据库。
开启“Development Mode”(开发模式)。
将
ownerId
用作可查询字段。启用同步。
定义规则,确定用户在使用Device Sync时拥有哪些权限。 对于此示例,我们分配了一个默认角色,该角色适用于任何没有特定于集合的角色的集合。 在此示例中,用户可以读取和写入数据,其中登录用户的
user.id
ownerId
与对象的 匹配:{ "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 } ] }
现在,部署您的应用程序更新。
提示
该应用的 Sync 版本对应用流程进行了少量改动。第一个屏幕变为 LoginView
。在您按 Log
in(登录)按钮时,该应用导航到 ItemsView,您可以在其中看到单个 itemGroup 中的同步项目列表。
在源文件的顶部,使用 您的 App ID 初始化可选的 Realm 应用 :
// 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)
提示
您可将应用引用更改为 nil
,以切换回仅本地(非 Device Sync)模式。
让我们更新主 ContentView,以在应用引用不是 nil
时显示 SyncContentView
:
/// 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() } } } }
我们对 syncContentView 的定义如下。
SyncContentView 会观察 Realm 应用实例。此应用实例是与 App Services 后端进行通信的接口,而它可提供 Sync 所需的用户身份验证。通过观察此应用实例,SyncContentView 可在用户登录或注销时进行回应。
此视图有两个可能的状态:
如果 Realm 应用当前没有登录用户,则显示
LoginView
。如果应用具有登录用户,则显示
OpenSyncedRealmView
。
在此视图中,在确认拥有用户后,我们将创建一个包含initialSubscriptions
参数的FlexibleSyncConfiguration() 。 我们可以使用此参数来订阅可可查询字段。 这些初始订阅搜索与查询匹配的数据,并将该数据同步到域。 如果没有数据与查询匹配,则域打开时初始为空状态。
客户端应用程序只能将与订阅查询匹配的对象写入通过 flexibleSyncConfiguration
打开的 Realm。尝试写入与查询不匹配的对象会导致此应用执行补偿写入,从而撤消非法写入操作。
/// 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() } } }
在我们的订阅中,我们会查询是否存在 ItemGroup
和 Item
对象;而在这些对象中,ownerId
会与已登录用户的 user.id
进行匹配。与上面启用 Device Sync 时所用的权限一起,这表示用户只能读取和写入自己的数据。
灵活同步不会自动提供对已链接对象的访问权限。因此,必须同时为 ItemGroup
和 Item
对象添加订阅,因为我们无法只查询其中之一便可获取相关对象。
从此时开始,我们会使用某一环境对象将 flexibleSyncConfiguration 作为 realmConfiguration
传递给 OpenSyncedRealmView。该视图为负责打开 Realm 和处理该数据的视图。Sync 使用此配置来搜索应同步到 Realm 的数据。
OpenSyncedRealmView() .environment(\.realmConfiguration, config)
登录后,使用 AsyncOpen 属性包装器异步打开域。
由于我们已将 flexibleSyncConfiguration()
作为环境值注入到此视图中,因此属性包装器会使用此配置来启动 Sync 并在打开 Realm 之前先下载所有匹配的数据。如果未提供配置,属性包装器则会为我们创建一个默认 flexibleSyncConfiguration()
,而我们也可在 .onAppear
中订阅查询。
// We've injected a `flexibleSyncConfiguration` as an environment value, // so `@AsyncOpen` here opens a realm using that configuration. YOUR_APP_SERVICES_APP_ID_HERE, timeout: 4000) var asyncOpen (appId:
OpenSyncedRealmView 打开AsyncOpenState枚举,它允许我们根据状态显示不同的视图。 在我们的示例中,当我们连接到应用并且域正在同步时,我们会显示ProgressView
。 然后,我们打开该域,将itemGroup
传递给ItemsView
;如果无法打开该域,则显示ErrorView
。
提示
此视图具有几种状态:
连接或等待登录时,显示
ProgressView
。向 Realm 下载更改时,显示带进度指示器的
ProgressView
。在 Realm 打开时,检查 itemGroup 对象。如果还不存在,请创建一个对象。然后,在 Realm 中显示 itemGroup 的 ItemsView。提供一个 ItemsView 可以在导航栏左上角显示的
LogoutButton
。如果加载 Realm 时出错,则显示包含此错误的错误视图。
当您运行此应用并看到主用户界面时,此视图不含任何项目。这是因为我们使用了匿名登录,因而此时为该特定用户的第一次登录。
/// This view opens a synced realm. struct OpenSyncedRealmView: View { // We've injected a `flexibleSyncConfiguration` as an environment value, // so `@AsyncOpen` here opens a realm using that configuration. YOUR_APP_SERVICES_APP_ID_HERE, timeout: 4000) var asyncOpen (appId: var body: some View { // Because we are setting the `ownerId` to the `user.id`, we need // access to the app's current user in this view. let user = app?.currentUser switch asyncOpen { // Starting the Realm.asyncOpen process. // Show a progress view. case .connecting: ProgressView() // Waiting for a user to be logged in before executing // Realm.asyncOpen. case .waitingForUser: ProgressView("Waiting for user to log in...") // The realm has been opened and is ready for use. // Show the content view. case .open(let realm): ItemsView(itemGroup: { if realm.objects(ItemGroup.self).count == 0 { try! realm.write { // Because we're using `ownerId` as the queryable field, we must // set the `ownerId` to equal the `user.id` when creating the object realm.add(ItemGroup(value: ["ownerId":user!.id])) } } return realm.objects(ItemGroup.self).first! }(), leadingBarButton: AnyView(LogoutButton())).environment(\.realm, realm) // The realm is currently being downloaded from the server. // Show a progress view. case .progress(let progress): ProgressView(progress) // Opening the Realm failed. // Show an error view. case .error(let error): ErrorView(error: error) } } }
在我们的订阅中,我们会查询是否存在 ItemGroup
和 Item
对象;而在这些对象中,ownerId
会与已登录用户的 user.id
进行匹配。与上面创建灵活同步应用时所用的权限一起,这表示用户只能读取和写入自己的数据。
灵活同步不会自动提供对已链接对象的访问权限。因此,必须同时为 ItemGroup
和 Item
对象添加订阅,因为我们无法只查询其中之一便可获取相关对象。
考虑到这一点,我们还须更新创建 ItemGroup
对象时所在的视图。我们必须将 ownerId
设为已登录用户的 user.id
。
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)
创建 Item
对象时,我们还须更新 ItemsView
以添加 ownerId
:
// 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()
使用 Atlas App Services 对用户进行身份验证
LoginView 会维持某一状态,以显示活动指示器或错误。单击 Log in anonymously 按钮,通过引用上述操作传入的 Realm 应用程序实例进行登录。
提示
在 LoginView 中,您可以实施电子邮件/密码身份验证或其他身份验证提供程序。为简便起见,此示例使用匿名身份验证。
登录完成后,LoginView 自身无需执行任何操作。因为父视图正在观察 Realm 应用,因此它会注意到用户身份验证状态何时出现变化,并决定显示 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) } } }
LogoutButton 的工作方式与 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) } }
登录后,此应用会遵循与仅本地版本相同的流程。
完整代码
如果您想在使用或不使用 Device Sync 的情况下分别进行复制和粘贴或检查完整代码,则请参阅下文。
import RealmSwift import SwiftUI // MARK: Models /// Random adjectives for more interesting demo item names let randomAdjectives = [ "fluffy", "classy", "bumpy", "bizarre", "wiggly", "quick", "sudden", "acoustic", "smiling", "dispensable", "foreign", "shaky", "purple", "keen", "aberrant", "disastrous", "vague", "squealing", "ad hoc", "sweet" ] /// Random noun for more interesting demo item names let randomNouns = [ "floor", "monitor", "hair tie", "puddle", "hair brush", "bread", "cinder block", "glass", "ring", "twister", "coasters", "fridge", "toe ring", "bracelet", "cabinet", "nail file", "plate", "lace", "cork", "mouse pad" ] /// An individual item. Part of an `ItemGroup`. final class Item: Object, ObjectKeyIdentifiable { /// The unique ID of the Item. `primaryKey: true` declares the /// _id member as the primary key to the realm. true) var _id: ObjectId (primaryKey: /// The name of the Item, By default, a random name is generated. var name = "\(randomAdjectives.randomElement()!) \(randomNouns.randomElement()!)" /// A flag indicating whether the user "favorited" the item. var isFavorite = false /// Users can enter a description, which is an empty string by default var itemDescription = "" /// The backlink to the `ItemGroup` this item is a part of. "items") var group: LinkingObjects<ItemGroup> (originProperty: } /// Represents a collection of items. final class ItemGroup: Object, ObjectKeyIdentifiable { /// The unique ID of the ItemGroup. `primaryKey: true` declares the /// _id member as the primary key to the realm. true) var _id: ObjectId (primaryKey: /// The collection of Items in this group. var items = RealmSwift.List<Item>() } extension Item { static let item1 = Item(value: ["name": "fluffy coasters", "isFavorite": false, "ownerId": "previewRealm"]) static let item2 = Item(value: ["name": "sudden cinder block", "isFavorite": true, "ownerId": "previewRealm"]) static let item3 = Item(value: ["name": "classy mouse pad", "isFavorite": false, "ownerId": "previewRealm"]) } extension ItemGroup { static let itemGroup = ItemGroup(value: ["ownerId": "previewRealm"]) static var previewRealm: Realm { var realm: Realm let identifier = "previewRealm" let config = Realm.Configuration(inMemoryIdentifier: identifier) do { realm = try Realm(configuration: config) // Check to see whether the in-memory realm already contains an ItemGroup. // If it does, we'll just return the existing realm. // If it doesn't, we'll add an ItemGroup and append the Items. let realmObjects = realm.objects(ItemGroup.self) if realmObjects.count == 1 { return realm } else { try realm.write { realm.add(itemGroup) itemGroup.items.append(objectsIn: [Item.item1, Item.item2, Item.item3]) } return realm } } catch let error { fatalError("Can't bootstrap item data: \(error.localizedDescription)") } } } // MARK: Views // MARK: Main Views /// The main screen that determines whether to present the SyncContentView or the LocalOnlyContentView. /// For now, it always displays the LocalOnlyContentView. @main struct ContentView: SwiftUI.App { var body: some Scene { WindowGroup { LocalOnlyContentView() } } } /// The main content view if not using Sync. struct LocalOnlyContentView: View { var searchFilter: String = "" // Implicitly use the default realm's objects(ItemGroup.self) ItemGroup.self) var itemGroups ( var body: some View { if let itemGroup = itemGroups.first { // Pass the ItemGroup objects to a view further // down the hierarchy ItemsView(itemGroup: itemGroup) } else { // For this small app, we only want one itemGroup in the realm. // You can expand this app to support multiple itemGroups. // For now, if there is no itemGroup, add one here. ProgressView().onAppear { $itemGroups.append(ItemGroup()) } } } } // MARK: Item Views /// The screen containing a list of items in an ItemGroup. Implements functionality for adding, rearranging, /// and deleting items in the ItemGroup. struct ItemsView: View { var itemGroup: ItemGroup /// The button to be displayed on the top left. var leadingBarButton: AnyView? var body: some View { NavigationView { VStack { // The list shows the items in the realm. List { ForEach(itemGroup.items) { item in ItemRow(item: item) }.onDelete(perform: $itemGroup.items.remove) .onMove(perform: $itemGroup.items.move) } .listStyle(GroupedListStyle()) .navigationBarTitle("Items", displayMode: .large) .navigationBarBackButtonHidden(true) .navigationBarItems( leading: self.leadingBarButton, // Edit button on the right to enable rearranging items trailing: EditButton()) // Action bar at bottom contains Add button. HStack { Spacer() Button(action: { // The bound collection automatically // handles write transactions, so we can // append directly to it. $itemGroup.items.append(Item()) }) { Image(systemName: "plus") } }.padding() } } } } struct ItemsView_Previews: PreviewProvider { static var previews: some View { let realm = ItemGroup.previewRealm let itemGroup = realm.objects(ItemGroup.self) ItemsView(itemGroup: itemGroup.first!) } } /// Represents an Item in a list. struct ItemRow: View { var item: Item var body: some View { // You can click an item in the list to navigate to an edit details screen. NavigationLink(destination: ItemDetailsView(item: item)) { Text(item.name) if item.isFavorite { // If the user "favorited" the item, display a heart icon Image(systemName: "heart.fill") } } } } /// Represents a screen where you can edit the item's name. struct ItemDetailsView: View { var item: Item var body: some View { VStack(alignment: .leading) { Text("Enter a new name:") // Accept a new name TextField("New name", text: $item.name) .navigationBarTitle(item.name) .navigationBarItems(trailing: Toggle(isOn: $item.isFavorite) { Image(systemName: item.isFavorite ? "heart.fill" : "heart") }) }.padding() } } struct ItemDetailsView_Previews: PreviewProvider { static var previews: some View { NavigationView { ItemDetailsView(item: Item.item2) } } }
import RealmSwift import SwiftUI // MARK: Atlas App Services (Optional) // The App Services App. Change YOUR_APP_SERVICES_APP_ID_HERE to your App Services App ID. // If you don't have a App Services App and don't wish to use Sync for now, // you can change this to: // let app: RealmSwift.App? = nil let app: RealmSwift.App? = RealmSwift.App(id: YOUR_APP_SERVICES_APP_ID_HERE) // MARK: Models /// Random adjectives for more interesting demo item names let randomAdjectives = [ "fluffy", "classy", "bumpy", "bizarre", "wiggly", "quick", "sudden", "acoustic", "smiling", "dispensable", "foreign", "shaky", "purple", "keen", "aberrant", "disastrous", "vague", "squealing", "ad hoc", "sweet" ] /// Random noun for more interesting demo item names let randomNouns = [ "floor", "monitor", "hair tie", "puddle", "hair brush", "bread", "cinder block", "glass", "ring", "twister", "coasters", "fridge", "toe ring", "bracelet", "cabinet", "nail file", "plate", "lace", "cork", "mouse pad" ] /// An individual item. Part of an `ItemGroup`. final class Item: Object, ObjectKeyIdentifiable { /// The unique ID of the Item. `primaryKey: true` declares the /// _id member as the primary key to the realm. true) var _id: ObjectId (primaryKey: /// The name of the Item, By default, a random name is generated. var name = "\(randomAdjectives.randomElement()!) \(randomNouns.randomElement()!)" /// A flag indicating whether the user "favorited" the item. var isFavorite = false /// Users can enter a description, which is an empty string by default var itemDescription = "" /// The backlink to the `ItemGroup` this item is a part of. "items") var group: LinkingObjects<ItemGroup> (originProperty: /// Store the user.id as the ownerId so you can query for the user's objects with Flexible Sync /// Add this to both the `ItemGroup` and the `Item` objects so you can read and write the linked objects. var ownerId = "" } /// Represents a collection of items. final class ItemGroup: Object, ObjectKeyIdentifiable { /// The unique ID of the ItemGroup. `primaryKey: true` declares the /// _id member as the primary key to the realm. true) var _id: ObjectId (primaryKey: /// The collection of Items in this group. var items = RealmSwift.List<Item>() /// Store the user.id as the ownerId so you can query for the user's objects with Flexible Sync /// Add this to both the `ItemGroup` and the `Item` objects so you can read and write the linked objects. var ownerId = "" } extension Item { static let item1 = Item(value: ["name": "fluffy coasters", "isFavorite": false, "ownerId": "previewRealm"]) static let item2 = Item(value: ["name": "sudden cinder block", "isFavorite": true, "ownerId": "previewRealm"]) static let item3 = Item(value: ["name": "classy mouse pad", "isFavorite": false, "ownerId": "previewRealm"]) } extension ItemGroup { static let itemGroup = ItemGroup(value: ["ownerId": "previewRealm"]) static var previewRealm: Realm { var realm: Realm let identifier = "previewRealm" let config = Realm.Configuration(inMemoryIdentifier: identifier) do { realm = try Realm(configuration: config) // Check to see whether the in-memory realm already contains an ItemGroup. // If it does, we'll just return the existing realm. // If it doesn't, we'll add an ItemGroup and append the Items. let realmObjects = realm.objects(ItemGroup.self) if realmObjects.count == 1 { return realm } else { try realm.write { realm.add(itemGroup) itemGroup.items.append(objectsIn: [Item.item1, Item.item2, Item.item3]) } return realm } } catch let error { fatalError("Can't bootstrap item data: \(error.localizedDescription)") } } } // MARK: Views // MARK: Main Views /// The main screen that determines whether to present the SyncContentView or the LocalOnlyContentView. @main struct ContentView: SwiftUI.App { var body: some Scene { WindowGroup { // Using Sync? if let app = app { SyncContentView(app: app) } else { LocalOnlyContentView() } } } } /// The main content view if not using Sync. struct LocalOnlyContentView: View { var searchFilter: String = "" // Implicitly use the default realm's objects(ItemGroup.self) ItemGroup.self) var itemGroups ( var body: some View { if let itemGroup = itemGroups.first { // Pass the ItemGroup objects to a view further // down the hierarchy ItemsView(itemGroup: itemGroup) } else { // For this small app, we only want one itemGroup in the realm. // You can expand this app to support multiple itemGroups. // For now, if there is no itemGroup, add one here. ProgressView().onAppear { $itemGroups.append(ItemGroup()) } } } } /// This view observes the Realm app object. /// Either direct the user to login, or open a realm /// with a logged-in user. struct SyncContentView: View { // Observe the Realm app object in order to react to login state changes. var app: RealmSwift.App var body: some View { if let user = app.currentUser { // Create a `flexibleSyncConfiguration` with `initialSubscriptions`. // We'll inject this configuration as an environment value to use when opening the realm // in the next view, and the realm will open with these initial subscriptions. let config = user.flexibleSyncConfiguration(initialSubscriptions: { subs in // Check whether the subscription already exists. Adding it more // than once causes an error. if let foundSubscriptions = subs.first(named: "user_groups") { // Existing subscription found - do nothing return } else { // Add queries for any objects you want to use in the app // Linked objects do not automatically get queried, so you // must explicitly query for all linked objects you want to include subs.append(QuerySubscription<ItemGroup>(name: "user_groups") { // Query for objects where the ownerId is equal to the app's current user's id // This means the app's current user can read and write their own data $0.ownerId == user.id }) subs.append(QuerySubscription<Item>(name: "user_items") { $0.ownerId == user.id }) } }) OpenSyncedRealmView() .environment(\.realmConfiguration, config) } else { // If there is no user logged in, show the login view. LoginView() } } } /// This view opens a synced realm. struct OpenSyncedRealmView: View { // We've injected a `flexibleSyncConfiguration` as an environment value, // so `@AsyncOpen` here opens a realm using that configuration. YOUR_APP_SERVICES_APP_ID_HERE, timeout: 4000) var asyncOpen (appId: var body: some View { // Because we are setting the `ownerId` to the `user.id`, we need // access to the app's current user in this view. let user = app?.currentUser switch asyncOpen { // Starting the Realm.asyncOpen process. // Show a progress view. case .connecting: ProgressView() // Waiting for a user to be logged in before executing // Realm.asyncOpen. case .waitingForUser: ProgressView("Waiting for user to log in...") // The realm has been opened and is ready for use. // Show the content view. case .open(let realm): ItemsView(itemGroup: { if realm.objects(ItemGroup.self).count == 0 { try! realm.write { // Because we're using `ownerId` as the queryable field, we must // set the `ownerId` to equal the `user.id` when creating the object realm.add(ItemGroup(value: ["ownerId":user!.id])) } } return realm.objects(ItemGroup.self).first! }(), leadingBarButton: AnyView(LogoutButton())).environment(\.realm, realm) // The realm is currently being downloaded from the server. // Show a progress view. case .progress(let progress): ProgressView(progress) // Opening the Realm failed. // Show an error view. case .error(let error): ErrorView(error: error) } } } struct ErrorView: View { var error: Error var body: some View { VStack { Text("Error opening the realm: \(error.localizedDescription)") } } } // MARK: Authentication Views /// Represents the login screen. We will have a button to log in anonymously. struct LoginView: View { // Hold an error if one occurs so we can display it. var error: Error? // Keep track of whether login is in progress. var isLoggingIn = false var body: some View { VStack { if isLoggingIn { ProgressView() } if let error = error { Text("Error: \(error.localizedDescription)") } Button("Log in anonymously") { // Button pressed, so log in isLoggingIn = true Task { do { let user = try await app!.login(credentials: .anonymous) // Other views are observing the app and will detect // that the currentUser has changed. Nothing more to do here. print("Logged in as user with id: \(user.id)") } catch { print("Failed to log in: \(error.localizedDescription)") // Set error to observed property so it can be displayed self.error = error return } } }.disabled(isLoggingIn) } } } struct LoginView_Previews: PreviewProvider { static var previews: some View { LoginView() } } /// A button that handles logout requests. struct LogoutButton: View { var isLoggingOut = false var body: some View { Button("Log Out") { guard let user = app!.currentUser else { return } isLoggingOut = true Task { do { try await app!.currentUser!.logOut() // Other views are observing the app and will detect // that the currentUser has changed. Nothing more to do here. } catch { print("Error logging out: \(error.localizedDescription)") } } }.disabled(app!.currentUser == nil || isLoggingOut) } } // MARK: Item Views /// The screen containing a list of items in an ItemGroup. Implements functionality for adding, rearranging, /// and deleting items in the ItemGroup. struct ItemsView: View { var itemGroup: ItemGroup /// The button to be displayed on the top left. var leadingBarButton: AnyView? var body: some View { let user = app?.currentUser NavigationView { VStack { // The list shows the items in the realm. List { ForEach(itemGroup.items) { item in ItemRow(item: item) }.onDelete(perform: $itemGroup.items.remove) .onMove(perform: $itemGroup.items.move) } .listStyle(GroupedListStyle()) .navigationBarTitle("Items", displayMode: .large) .navigationBarBackButtonHidden(true) .navigationBarItems( leading: self.leadingBarButton, // Edit button on the right to enable rearranging items trailing: EditButton()) // Action bar at bottom contains Add button. HStack { Spacer() Button(action: { // The bound collection automatically // handles write transactions, so we can // append directly to it. // Because we are using Flexible Sync, we must set // the item's ownerId to the current user.id when we create it. $itemGroup.items.append(Item(value: ["ownerId":user!.id])) }) { Image(systemName: "plus") } }.padding() } } } } struct ItemsView_Previews: PreviewProvider { static var previews: some View { let realm = ItemGroup.previewRealm let itemGroup = realm.objects(ItemGroup.self) ItemsView(itemGroup: itemGroup.first!) } } /// Represents an Item in a list. struct ItemRow: View { var item: Item var body: some View { // You can click an item in the list to navigate to an edit details screen. NavigationLink(destination: ItemDetailsView(item: item)) { Text(item.name) if item.isFavorite { // If the user "favorited" the item, display a heart icon Image(systemName: "heart.fill") } } } } /// Represents a screen where you can edit the item's name. struct ItemDetailsView: View { var item: Item var body: some View { VStack(alignment: .leading) { Text("Enter a new name:") // Accept a new name TextField("New name", text: $item.name) .navigationBarTitle(item.name) .navigationBarItems(trailing: Toggle(isOn: $item.isFavorite) { Image(systemName: item.isFavorite ? "heart.fill" : "heart") }) }.padding() } } struct ItemDetailsView_Previews: PreviewProvider { static var previews: some View { NavigationView { ItemDetailsView(item: Item.item2) } } }