SwiftUI QuickStart와 함께 Realm 사용
전제 조건
Xcode 12.4 이상(최소 Swift 버전 5.3.1)이 있어야 합니다.
최소 iOS 타겟이 15.0인 SwiftUI '앱' 템플릿을 사용하여 새 Xcode 프로젝트를 만듭니다.
Swift SDK 설치 이 SwiftUI 앱에는 최소 10.19.0의 SDK 버전이 필요합니다.
개요
팁
Swift UI와 함께 Realm 사용을 참조하세요.
이 페이지에서는 Realm과 SwiftUI를 빠르게 시작할 수 있는 간단한 작업 앱을 제공합니다. Realm의 SwiftUI 기능에 대한 자세한 설명을 포함한 추가 예시를 보려면 SwiftUI - Swift SDK를 참조하세요.
이 페이지에는 작동하는 Realm 및 SwiftUI 앱의 모든 코드가 포함되어 있습니다. 앱이 ItemsView
에서 시작되며, 여기서 항목 목록을 편집할 수 있습니다.
화면 오른쪽 하단의
Add
버튼을 눌러 무작위로 생성된 항목을 추가합니다.앱이 영역에 유지되는 목록 순서를 수정하려면 오른쪽 상단의
Edit
버튼을 누르세요.스와이프하여 항목을 삭제할 수도 있습니다.
목록에 항목이 있으면 항목 중 하나를 눌러 ItemDetailsView
로 이동할 수 있습니다. 여기에서 항목 이름을 수정하거나 즐겨찾기로 표시할 수 있습니다.
화면 중앙에 있는 텍스트 필드를 누르고 새 이름을 입력합니다. Return 키를 누르면 항목 이름이 앱 전체에서 업데이트됩니다.
오른쪽 상단에 있는 하트 토글을 눌러 즐겨찾기 상태를 전환할 수도 있습니다.
팁
이 가이드 는 선택적으로 Device Sync 와 통합됩니다. 아래의 Atlas Device Sync 통합 을 참조하세요.
시작하기
SwiftUI '앱' 템플릿을 사용하여 Xcode 프로젝트를 만들었다고 가정합니다. 기본 Swift 파일을 열고 Xcode가 생성한 @main
App
클래스를 포함하여 내부의 모든 코드를 삭제합니다. 파일 상단에서 Realm 및 SwiftUI 프레임워크를 가져옵니다:
import RealmSwift import SwiftUI
팁
전체 코드를 바로 사용하고 싶으신가요? 아래의 전체 코드 로 이동하세요.
모델 정의
일반적인 Realm 데이터 모델링 사용 사례는 '사물'과 '사물의 컨테이너'를 갖는 것입니다. 이 앱은 두 가지 관련 Realm 객체 모델, 즉 항목과 itemGroup을 정의합니다.
항목에는 다음과 같은 사용자 관련 속성이 두 가지 있습니다.
임의로 생성된 이름이며 사용자가 편집할 수 있습니다.
사용자가 항목을 '즐겨찾기'에 추가했는지 여부를 보여 주는
isFavorite
부울 속성입니다.
itemGroup에는 항목이 포함됩니다. itemGroup을 확장하여 특정 사용자와의 이름 및 연결을 가질 수 있지만 이는 이 가이드의 범위를 벗어납니다.
다음 코드를 기본 Swift 파일에 붙여넣어 모델을 정의합니다.
Flexible Sync는 연결된 객체를 자동으로 포함하지 않으므로 두 객체에 ownerId
을(를) 추가해야 합니다. 로컬 영역만 사용하려는 경우 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() } } }
팁
기본 영역이 아닌 다른 영역은 보기 계층의 상위 환경에서 환경 객체를 전달하여 사용할 수 있습니다.
LocalOnlyContentView() .environment(\.realmConfiguration, Realm.Configuration( /* ... */ ))
LocalOnlyContentView에는 @ObservedResults itemGroups가 있습니다. 이는 기본 Realm을 암시적으로 사용하여 보기가 나타날 때 모든 itemGroups를 로드합니다.
이 앱은 하나의 itemGroup만 있을 것으로 예상합니다. 영역에 itemGroup이 있는 경우 LocalOnlyContentView는 해당 itemGroup에 대한 ItemsView
를 렌더링합니다.
영역에 itemGroup이 아직 없으면 LocalOnlyContentView는 ProgressView를 추가하는 동안 ProgressView를 표시합니다. 보기는 @ObservedResults
속성 래퍼(wrapper) 덕분에 itemGroups를 관찰하므로 첫 번째 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
속성 래퍼(wrapper)를 사용합니다. 이러한 클래스는 속성 래퍼를 사용하여 속성을 표시하고 업데이트하는 방법에 대한 몇 가지 예를 더 보여 줍니다.
/// 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
은(는) 정지된 객체입니다. 쓰기 트랜잭션(write transaction)에서 @ObservedRealmObject
의 속성을 직접 수정하려면 먼저 .thaw()
을(를) 수정해야 합니다.
이제 Realm과 SwiftUI를 사용하기 위해 필요한 것을 모두 갖추었습니다. 테스트를 실행하여 예상대로 모두 작동하는지 확인합니다. 이 앱을 Device Sync와 통합하는 방법을 알아보려면 계속해서 읽어주세요.
Atlas Device Sync 통합
이제 Realm 앱이 작동하므로 선택적으로 Device Sync와 통합할 수 있습니다. 동기화를 사용하면 여러 장치에서 변경한 내용을 확인할 수 있습니다. 이 앱에 동기화를 추가하려면 먼저 다음을 확인하세요.
클러스터와 데이터베이스를 지정합니다.
개발 모드를 켭니다.
쿼리 가능한 필드로
ownerId
을(를) 사용합니다.동기화를 활성화합니다.
Device Sync 를 사용할 때 사용자에게 어떤 권한이 있는지 결정하는 규칙을 정의합니다 . 이 예시 에서는 컬렉션별 역할 이 없는 모든 컬렉션 에 적용되는 기본값 역할 을 할당합니다. 이 예시 에서
user.id
사용자는 로그인한 사용자의ownerId
가 객체 의 와 일치하는 데이터를 읽고 쓰기 (write) 수 있습니다.{ "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 } ] }
이제 애플리케이션 업데이트를 배포하세요.
팁
이 앱의 동기화 버전은 앱 흐름을 약간 변경합니다. 첫 번째 화면은 LoginView
가 됩니다. Log
in 버튼을 누르면 앱이 ItemsView로 이동하며 여기에서 단일 itemGroup의 동기화된 항목 목록을 볼 수 있습니다.
소스 파일 상단에서 앱 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
(으)로 변경하여 로컬 전용(비기기 동기화) 모드로 다시 전환할 수 있습니다.
앱 참조가 nil
이 아닌 경우 SyncContentView
를 표시하도록 메인 ContentView를 다음과 같이 업데이트합니다.
/// 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 백 엔드의 인터페이스입니다. SyncContentView는 앱 인스턴스를 관찰하여 사용자가 로그인하거나 로그아웃할 때 반응할 수 있습니다.
이 보기에는 다음과 같은 두 가지 가능한 상태가 있습니다.
Realm 앱에 현재 로그인한 사용자가 없다면
LoginView
를 표시합니다.앱에 로그인한 사용자가 있는 경우
OpenSyncedRealmView
를 표시합니다.
이 뷰에서는 사용자가 있는지 확인한 후 initialSubscriptions
매개 변수를 포함하는 flexibleSyncConfiguration() 을 생성합니다. 이 매개변수를 사용 하여 쿼리 가능 필드 를 구독 할 수 있습니다. 이러한 초기 구독은 쿼리와 일치하는 데이터를 검색 하고 해당 데이터를 영역 에 동기화합니다. 쿼리와 일치하는 데이터가 없는 경우 영역 은 초기의 빈 상태 로 열립니다.
클라이언트 애플리케이션은 flexibleSyncConfiguration
(으)로 열린 영역에 대한 구독 쿼리와 일치하는 객체만 쓸 수 있습니다. 쿼리와 일치하지 않는 객체를 쓰려고 하면 앱이 보상 쓰기를 수행하여 잘못된 쓰기 작업을 실행 취소합니다.
/// 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() } } }
구독에서는 ownerId
이(가) 로그인한 사용자의 user.id
와(과) 일치하는 ItemGroup
및 Item
객체를 쿼리합니다. 위에서 Device Sync를 활성화할 때 사용한 권한과 함께 사용자는 자신의 데이터만 읽고 쓸 수 있습니다.
Flexible Sync는 연결된 객체에 대한 액세스를 자동으로 제공하지 않습니다. 이 때문에 ItemGroup
및 Item
객체 모두에 대한 구독을 추가해야 합니다. 둘 중 하나만 쿼리하고 관련 객체를 가져올 수는 없습니다.
여기에서 환경 객체를 사용하여 유연한 SyncConfiguration을 OpenSyncedRealmView에 realmConfiguration
(으)로 전달합니다. 이 보기는 영역을 열고 데이터 작업을 담당하는 보기입니다. 동기화는 이 구성을 사용하여 영역에 동기화해야 하는 데이터를 검색합니다.
OpenSyncedRealmView() .environment(\.realmConfiguration, config)
로그인한 후에는 AsyncOpen 속성 래퍼를 사용하여 비동기적으로 영역을 엽니다.
보기에 환경 값으로 flexibleSyncConfiguration()
을(를) 주입했기 때문에 속성 래퍼(wrapper)는 이 구성을 사용하여 영역을 열기 전에 동기화를 시작하고 일치하는 데이터를 다운로드합니다. 구성을 제공하지 않았다면 속성 래퍼(wrapper)가 기본 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
을(를) 표시합니다.영역에 변경 사항을 다운로드하는 동안 진행률 표시기와 함께
ProgressView
을(를) 표시합니다.영역이 열리면 itemGroup 객체를 확인합니다. 아직 생성되지 않았다면 생성해 줍니다. 그런 다음 영역의 itemGroup에 대한 ItemsView를 표시합니다. ItemsView가 탐색 모음의 왼쪽 상단에 표시할 수 있는
LogoutButton
을 제공합니다.영역을 로드하는 동안 오류가 발생하면 오류가 포함된 오류 보기를 표시합니다.
앱을 실행하고 기본 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) } } }
구독에서는 ownerId
이(가) 로그인한 사용자의 user.id
와(과) 일치하는 ItemGroup
및 Item
객체를 쿼리합니다. 위에서 Flexible Sync 앱을 만들 때 사용한 권한과 함께 사용자는 자신의 데이터만 읽고 쓸 수 있습니다.
Flexible Sync는 연결된 객체에 대한 액세스를 자동으로 제공하지 않습니다. 이 때문에 ItemGroup
및 Item
객체 모두에 대한 구독을 추가해야 합니다. 둘 중 하나만 쿼리하고 관련 객체를 가져올 수는 없습니다.
이 점을 염두에 두고 여기에서 ItemGroup
객체를 만드는 뷰도 업데이트해야 합니다. 로그인한 사용자의 user.id
(으)로 ownerId
을(를) 설정해야 합니다.
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
객체를 생성할 때 ownerId
을(를) 추가하려면 ItemsView
을(를) 업데이트해야 합니다.
// 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) } } }