Docs Menu
Docs Home
/ /
Atlas Device SDK
/ /

SwiftUI QuickStart와 함께 Realm 사용

이 페이지의 내용

  • 전제 조건
  • 개요
  • 시작하기
  • 모델 정의
  • 보기 및 관찰된 객체
  • Atlas Device Sync 통합
  • Atlas App Services로 사용자 인증
  • 전체 코드
  • 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.
@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 = ""
}

앱의 진입점은 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 {
@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())
}
}
}
}

SDK 버전 10.12.0부터 @ObservedResults와(과) 함께 선택적 키 경로 매개변수를 사용하여 제공된 키 경로 또는 키 경로에서 발생하는 변경 알림만 필터링할 수 있습니다. 예시:

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

ItemsView는 상위 보기에서 itemGroup을 받아 @ObservedRealmObject 속성에 저장합니다. 이렇게 하면 ItemsView가 변경된 위치에 관계없이 객체가 변경된 시기를 '알 수' 있습니다.

ItemsView는 itemGroup의 항목을 반복하고 각 항목을 목록으로 렌더링하기 위해 ItemRow에 전달합니다.

사용자가 행을 삭제하거나 이동할 때 어떤 일이 발생하는지 정의하기 위해 Realm 목록removemove 메서드를 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 {
@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()
}
}
}
}

마지막으로 ItemRowItemDetailsView 클래스는 위에서 전달된 항목과 함께 @ObservedRealmObject 속성 래퍼(wrapper)를 사용합니다. 이러한 클래스는 속성 래퍼를 사용하여 속성을 표시하고 업데이트하는 방법에 대한 몇 가지 예를 더 보여 줍니다.

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

@ObservedRealmObject 은(는) 정지된 객체입니다. 쓰기 트랜잭션(write transaction)에서 @ObservedRealmObject속성을 직접 수정하려면 먼저 .thaw()을(를) 수정해야 합니다.

이제 Realm과 SwiftUI를 사용하기 위해 필요한 것을 모두 갖추었습니다. 테스트를 실행하여 예상대로 모두 작동하는지 확인합니다. 이 앱을 Device Sync와 통합하는 방법을 알아보려면 계속해서 읽어주세요.

이제 Realm 앱이 작동하므로 선택적으로 Device Sync와 통합할 수 있습니다. 동기화를 사용하면 여러 장치에서 변경한 내용을 확인할 수 있습니다. 이 앱에 동기화를 추가하려면 먼저 다음을 확인하세요.

  • App Services 앱을 생성합니다.

  • 익명 인증을 활성화합니다.

  • Device Sync를 활성화합니다.

    1. Flexible Sync선택

    2. 클러스터와 데이터베이스를 지정합니다.

    3. 개발 모드를 켭니다.

    4. 쿼리 가능한 필드로 ownerId을(를) 사용합니다.

    5. 동기화를 활성화합니다.

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

구독에서는 ownerId이(가) 로그인한 사용자의 user.id와(과) 일치하는 ItemGroupItem 객체를 쿼리합니다. 위에서 Device Sync를 활성화할 때 사용한 권한과 함께 사용자는 자신의 데이터만 읽고 쓸 수 있습니다.

Flexible Sync는 연결된 객체에 대한 액세스를 자동으로 제공하지 않습니다. 이 때문에 ItemGroupItem 객체 모두에 대한 구독을 추가해야 합니다. 둘 중 하나만 쿼리하고 관련 객체를 가져올 수는 없습니다.

여기에서 환경 객체를 사용하여 유연한 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.
@AsyncOpen(appId: YOUR_APP_SERVICES_APP_ID_HERE, timeout: 4000) var asyncOpen

OpenSyncedRealmView는 AsyncOpenState 열거형 을 켜서 상태 에 따라 다른 뷰를 표시할 수 있습니다. 이 예시 에서는 앱에 연결하고 영역 이 동기화되는 동안 ProgressView 을 표시합니다. 그런 다음 영역 을 열고 itemGroupItemsView 에 전달하거나 영역 을 열 수 없는 경우 ErrorView 을 표시합니다.

동기화된 Realm을 열 때 AsyncOpen 속성 래퍼를 사용하여 Realm을 열기 전에 항상 동기화된 변경 사항을 다운로드하거나 AutoOpen 속성 래퍼를 사용하여 백그라운드에서 동기화하는 동안 Realm을 엽니다. AsyncOpen 은 사용자가 온라인 상태여야 하지만 AutoOpen 는 사용자가 오프라인 상태에서도 영역을 엽니다.

이 보기에는 다음과 같은 몇 가지 다른 상태가 있습니다.

  • 연결 중이거나 로그인을 기다리는 동안 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.
@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)
}
}
}

구독에서는 ownerId이(가) 로그인한 사용자의 user.id와(과) 일치하는 ItemGroupItem 객체를 쿼리합니다. 위에서 Flexible Sync 앱을 만들 때 사용한 권한과 함께 사용자는 자신의 데이터만 읽고 쓸 수 있습니다.

Flexible Sync는 연결된 객체에 대한 액세스를 자동으로 제공하지 않습니다. 이 때문에 ItemGroupItem 객체 모두에 대한 구독을 추가해야 합니다. 둘 중 하나만 쿼리하고 관련 객체를 가져올 수는 없습니다.

이 점을 염두에 두고 여기에서 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()

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

LogoutButton은 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