Docs Menu
Docs Home
/ /
Atlas Device SDK
/ /

SwiftUI クイックスタートによる Realm

項目一覧

  • 前提条件
  • Overview
  • はじめる
  • モデルを定義する
  • ビューと観察されたオブジェクト
  • Atlas Device Sync の統合
  • Atlas App Services によるユーザーの認証
  • 完全なコード
  • Xcode 12.4 以降(Swift バージョン 5.3.1 以降)を使用します。

  • SwiftUI「App」テンプレートを使用して、最小 iOS ターゲットが 15.0 の新しい Xcode プロジェクトを作成します。

  • Swift SDK をインストールします。 この SwiftUI アプリには、最小の SDK バージョン 10.19.0 が必要です。

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.
@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 = ""
}

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

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

最後に、 ItemRowクラスとItemDetailsViewクラスは、上記から渡されたアイテムを含む@ObservedRealmObjectプロパティ ラッパーを使用します。 これらのクラスは、プロパティ ラッパーを使用してプロパティを表示および更新する方法のさらに多くの例を示します。

/// 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 は、固定オブジェクトです。 書き込みトランザクションで@ObservedRealmObjectのプロパティを直接変更する場合は、まず.thaw()である必要があります。

この時点で、Realm と SwiftUI を使用するために必要なものはすべて揃っています。 それをテストして、すべてが期待どおりに動作しているかどうかを確認します。 このアプリを Device Sync と統合する方法については、 を参照してください。

Realm アプリが動作するようになったので、オプションで Device Sync と統合できます。 同期を使用すると、デバイス全体で行った変更を確認できます。 このアプリに同期を追加する前に、以下を確認してください。

  • App Services Appを作成します。

  • 匿名認証を有効にします。

  • Device Sync を有効にします。

    1. Flexible Sync を選択する

    2. クラスターとデータベースを指定します。

    3. 開発モードをオンにします。

    4. クエリ可能な フィールドとしてownerIdを使用します。

    5. 同期を有効にします。

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

サブスクライブでは、ItemGroup ItemownerIdがログイン ユーザーの と一致する オブジェクトと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.
@AsyncOpen(appId: YOUR_APP_SERVICES_APP_ID_HERE, timeout: 4000) var asyncOpen

OpenSyncedRealmView はAsyncOpenState 列挙型のスイッチです。これにより、状態に基づいてさまざまなビューを表示できます。 この例では、アプリに接続し、Realm を同期している間にProgressViewが表示されています。 次に、 itemGroupItemsViewに渡して Realm を開きます。Realm を開くことができない場合はErrorViewを表示します。

Tip

同期された Realm を開くときは、Realm を開く前にAsyncOpenプロパティ ラッパーを使用して常に同期された変更をダウンロードするか、AutoOpen プロパティラッパーを使用してバックグラウンドで同期中にRealmを開きます。 AsyncOpenではユーザーがオンラインである必要があり、 AutoOpenではユーザーがオフラインでも Realm が開きます。

このビューには、いくつかの異なる状態があります。

  • 接続中、またはログインを待機している間は、 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.
@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)
}
}
}

サブスクライブでは、ItemGroup ItemownerIdがログイン ユーザーの と一致する オブジェクトと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 ownerIdItemオブジェクトを作成するときに、 をアップデートして を追加する必要があります。

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

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

Logoutボタンは 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)
}
}

ログインすると、アプリはローカル専用バージョンと同じフローに従います。

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

戻る

SwiftUI