SwiftUI クイックスタートによる Realm
前提条件
Xcode 12.4 以降(Swift バージョン 5.3.1 以降)を使用します。
SwiftUI「App」テンプレートを使用して、最小 iOS ターゲットが 15.0 の新しい Xcode プロジェクトを作成します。
Swift SDK をインストールします。 この SwiftUI アプリには、最小の SDK バージョン 10.19.0 が必要です。
Overview
Tip
「 SwiftUI で Realm を使用する 」も参照してください
このページでは、Realm と SwiftUI を使用してすぐに起動して実行するための小さな動作するアプリを提供します。 Realm の SwiftUI 機能について、さらに説明した例えを希望する場合は、 SwiftUI - Swift SDK を参照してください。
このページには、動作する Realm と SwiftUI アプリのすべてのコードが含まれています。 このアプリは ItemsView
で起動し、アイテムのリストを編集できます。
ランダムに生成されたアイテムを追加するには、画面右下にある
Add
ボタンを押します。アプリが Realm に永続化するリスト順序を変更するには、右上の
Edit
ボタンを押します。スワップして項目を削除することもできます。
When you have items in the list, you can press one of the items to navigate to the ItemDetailsView
. ここで、アイテム名を変更したり、お気に入りとしてマークしたりできます。
画面中央のテキスト フィールドを押し、新しい名前を入力します。 [ Return ] を押すと、アイテム名がアプリ全体で更新されます。
右上のハートビートを押して、お気に入りのステータスを切り替えることもできます。
Tip
このガイドはオプションで、 Device Syncと統合します。 以下の「 Atlas Device Sync の統合」を参照してください。
はじめる
SwiftUI「App」テンプレートを使用して Xcode プロジェクトを作成したとします。 メインの Swift ファイルを開き、Xcode により生成された@main
App
クラスを含むすべてのコード内のすべてのコードを削除します。 ファイルの上部で、Realm と SwiftUI フレームワークをインポートします。
import RealmSwift import SwiftUI
Tip
完全なコードを使用してすぐに調べたい場合は、 以下の完全なコードに移動します。
モデルを定義する
Realm データ モデリングの一般的なユースケースは、「もの」と「もののコンテナ」を持つことです。 このアプリは、itemと itemsGroup の 2 つの関連する Realm オブジェクトモデルを定義します。
アイテムにはユーザー向けの 2 つのプロパティがあります。
ランダムに生成された名前。ユーザーが編集できる
ユーザーがアイテムを「お気に入り」したかどうかを示す
isFavorite
ブール値プロパティ。
itemsGroup にはアイテムが含まれます。 itemsGroup を拡張して、名前を設定し、特定のユーザーに関連付けることはできますが、これはこのガイドの範囲外です。
次のコードをメインの Swift ファイルに貼り付けて、モデルを定義します。
Flexible Sync にはリンクされたオブジェクトが自動的に含まれないため、両方のオブジェクトに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 = "" }
ビューと観察されたオブジェクト
The entrypoint of the app is the ContentView
class that derives from SwiftUI.App
. 現在は、常に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() } } }
Tip
ビュー階層の上位から環境オブジェクトを渡すことで、デフォルト以外の Realm を使用できます。
LocalOnlyContentView() .environment(\.realmConfiguration, Realm.Configuration( /* ... */ ))
LocalOnlyContentView には@ObservedResults itemsGroups があります。 これにより、ビューが表示されるときに、デフォルトの Realm を使用してすべての itemsGroup がロードされます。
このアプリでは、 itemsGroup が 1 つあることのみが想定されています。 Realm に itemsGroup がある場合、 LocalOnlyContentView はその itemsGroup のItemsView
をレンダリングします。
Realm に itemsGroup がまだ存在しない場合、 LocalOnlyContentView はプログレスビューを追加するときに プログレスビューを表示します。 ビューは@ObservedResults
プロパティ ラッパーの効果によって itemsGroup を監視するため、ビューは最初の itemsGroup を追加するとすぐに更新され、ItemView が表示されます。
/// 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
SDK バージョン 10.12.0 以降では、 @ObservedResults
とともに任意のキーパス パラメーターを使用して、指定されたキー パスまたはキー パスで発生する変更通知のみをフィルタリングできます。 例:
@ObservedResults(MyObject.self, keyPaths: ["myList.property"])
ItemsView は親ビューから itemsGroup を受け取り、 @ObservedRealmObjectプロパティに保存します。 これにより、ItemsView は、変更が発生した場所に関係なく、オブジェクトが変更されたときを「認識」できます。
ItemsView は itemsGroup のアイテムを反復処理し、各アイテムをItemRow
に渡し、リストとしてレンダリングします。
ユーザーが行を削除または移動したときに何が起こるかを定義するために、 Realmリストの remove
メソッドと move
メソッドを、それぞれの削除および移動イベントのハンドラーとしてSwiftUIます。 @ObservedRealmObject
プロパティ ラッパーの代わりに、書込みトランザクションを明示的に開始せずにこれらのメソッドを使用できます。 プロパティ ラッパーは、必要に応じて書込みトランザクションを自動的に開きます。
/// 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() } }
Tip
@ObservedRealmObject
は、固定オブジェクトです。 書き込みトランザクションで@ObservedRealmObject
のプロパティを直接変更する場合は、まず.thaw()
である必要があります。
この時点で、Realm と SwiftUI を使用するために必要なものはすべて揃っています。 それをテストして、すべてが期待どおりに動作しているかどうかを確認します。 このアプリを Device Sync と統合する方法については、 を参照してください。
Atlas Device Sync の統合
Realm アプリが動作するようになったので、オプションで Device Sync と統合できます。 同期を使用すると、デバイス全体で行った変更を確認できます。 このアプリに同期を追加する前に、以下を確認してください。
Flexible Sync を選択する
クラスターとデータベースを指定します。
開発モードをオンにします。
クエリ可能な フィールドとして
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 } ] }
ここで、アプリケーションの更新を配置します。
Tip
このアプリの同期バージョンでは、アプリフローが少し変更されます。 最初の画面はLoginView
になります。 Log
inボタンを押すと、アプリは ItemsView に移動され、単一の itemsGroup 内のアイテムの同期されたリストが表示されます。
ソース ファイルの上部で、 アプリ 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)
Tip
アプリ参照をnil
に変更すると、ローカル専用(Device Sync 以外)モードに切り替えることができます。
アプリ参照が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 インスタンスは、同期に必要なユーザー認証を提供する App Services バックエンドへのインターフェースです。 アプリ インスタンスを監視することで、SyncContentView はユーザーがログインまたはログアウトしたときにReactできます。
このビューには、次の 2 つの状態があります。
Realm アプリに現在ログインしているユーザーがない場合は、
LoginView
を表示します。アプリにログイン ユーザーが存在する場合は、
OpenSyncedRealmView
が表示されます。
このビューでは、ユーザーがあることを確認した後、 initialSubscriptions
パラメータを含むFlexibleSyncConfiguration()を作成します。 このパラメータを使用して、 クエリ可能なフィールド をサブスクライブできます。 これらの初期サブスクリプションでは、クエリに一致するデータを検索し、そのデータを Realm に同期します。 クエリに一致するデータがない場合、Realm は初期空の状態で開きます。
クライアント アプリケーションは、 flexibleSyncConfiguration
で開かれた Realm へのサブスクライブ クエリに一致するオブジェクトのみを書き込みできます。 クエリに一致しないオブジェクトを書込み (write) しようとすると、アプリは不正な書込み (write) 操作を元に戻すための 置換書込み (write) を実行します。
/// 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 を有効にしたときに使用した権限とともに、ユーザーは自分のデータの読み取りと書込みのみを実行できます。
Flexible Sync では、リンクされたオブジェクトへのアクセスが自動的に提供されることはありません。 このため、 オブジェクトとItemGroup
Item
オブジェクトの両方のサブスクリプションを追加する必要があります。どちらか一方をクエリして、関連するオブジェクトを取得することはできません。
ここから、環境オブジェクトを使用して、FlexibleSyncConfiguration をrealmConfiguration
として OpenSyncedRealmView に渡します。 これは、Realm を開き、データを操作するビューです。 Sync はこの構成を使用して、Realm に同期する必要があるデータを検索します。
OpenSyncedRealmView() .environment(\.realmConfiguration, config)
ログインすると、 AsyncOpenプロパティ ラッパーを使用してRealmを非同期に開きます。
環境値としてビューにflexibleSyncConfiguration()
を挿入しているため、プロパティ ラッパーはこの構成を使用して同期を開始し、一致するデータをダウンロードしてから、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 列挙型のスイッチです。これにより、状態に基づいてさまざまなビューを表示できます。 この例では、アプリに接続し、Realm を同期している間にProgressView
が表示されています。 次に、 itemGroup
をItemsView
に渡して Realm を開きます。Realm を開くことができない場合はErrorView
を表示します。
Tip
このビューには、いくつかの異なる状態があります。
接続中、またはログインを待機している間は、
ProgressView
が表示されます。Realm への変更をダウンロードしている間は、進行状況インジケーターとともに
ProgressView
が表示されます。Realm が開き、 itemsGroup オブジェクトを確認します。 がまだ存在しない場合は、作成します。 次に、Realm 内の itemsGroup の ItemsView を表示します。 itemsView がナビゲーション バーの左上に表示できる
LogoutButton
を指定します。Realm のロード中にエラーが発生した場合は、エラーを含むエラービューが表示されます。
アプリを実行してメイン UI を表示すると、ビューに項目は表示されません。 これは、匿名ログインを使用しているため、この特定のユーザーが初めてログインするときです。
/// 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
オブジェクトをクエリしています。上記の Flexible Sync アプリを作成したときに使用した権限とともに、ユーザーは自分のデータの読み取りと書込みのみを実行できます。
Flexible Sync では、リンクされたオブジェクトへのアクセスが自動的に提供されることはありません。 このため、 オブジェクトと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)
また、ItemsView
ownerId
Item
オブジェクトを作成するときに、 をアップデートして を追加する必要があります。
// Action bar at bottom contains Add button. HStack { Spacer() Button(action: { // The bound collection automatically // handles write transactions, so we can // append directly to it. // Because we are using Flexible Sync, we must set // the item's ownerId to the current user.id when we create it. $itemGroup.items.append(Item(value: ["ownerId":user!.id])) }) { Image(systemName: "plus") } }.padding()
Atlas App Services によるユーザーの認証
LoginView は、アクティビティ インジケーターまたはエラーを表示するためにいくつかの状態を維持します。 上記から渡された Realm アプリ インスタンスへの参照を使用して、 Log in anonymouslyボタンがクリックされたときにログインします。
Tip
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) } } }
Logoutボタンは 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) } } }