🗄️

SwiftUIでのRealmの使い方まとめ(プロパティラッパー、フェッチ、プレビュー、etc.)

2022/01/04に公開

はじめに

この記事は、Realm初心者の僕がSwiftUIでRealmを使うにあたってつまずいたところを主にまとめた物です。新たな知見があれば追記していくつもりです。間違いなどありましたら気軽にコメント欄でご指摘ください。

プロジェクトの例として、DiaryとPageというオブジェクトで構成される日記アプリを説明で用います。Diaryは複数のPageを内包する、というぐらいの認識で大丈夫です。

導入方法(XcodeプロジェクトでSwiftPM)

https://github.com/realm/realm-swift

Xcodeのサイドバーのプロジェクト -> PROJECT -> Package Dependencies -> "+"ボタンをクリック -> サーチフィールドに"https://github.com/realm/realm-swift"を入力 -> 右下の"Add Package"ボタンをクリック

オブジェクトの定義

Realmで扱いたいオブジェクトは、Objectを継承したclassとして宣言します。それぞれのプロパティには@Persistedというプロパティラッパーを付与します。

プロパティタイプは、BoolIntInt8Int16 Int32Int64DoubleFloatStringDateDataがサポートされています。

オブジェクトを識別するためのプライマリーキーには引数のprimaryKeytrueとします。ここではUUID().uuidStringを使用していますが、一意性が担保されていてRealmで保存できるプロパティタイプならなんでもいいようです。公式チュートリアルではRealmSwift.ObjectIdをプライマリーキーに設定していますが、SwiftUIのセレクションやナビゲーションなどでIDをSceneStorageなどに保存することを考えれば、Stringの方が便利なのではないでしょうか。

One-to-Manyのリレーションについては、親オブジェクトの方でRealmSwift.List、子オブジェクトの方でLinkingObjectsとして宣言します(Core Dataでいうところのinverse)。RealmSwift.Listについては、SwiftUIにも同名のタイプが存在するのでモジュール名を省略できません。

日記アプリのモデルを実装した例
import Foundation
import RealmSwift

final class Diary: Object, Identifiable {
    @Persisted(primaryKey: true) var id = UUID().uuidString
    
    @Persisted var name: String
    @Persisted var pages: RealmSwift.List<Page>
    
    convenience init(name: String, pages: [Page] = []) {
        self.init()
        self.name = name
        self.pages = .init()
        self.pages.append(objectsIn: pages)
    }
}

final class Page: Object, Identifiable {
    @Persisted(primaryKey: true) var id = UUID().uuidString
    
    @Persisted var title: String
    @Persisted var body: String
    @Persisted var createdDate: Date
    @Persisted var isMarked = false
    
    @Persisted(originProperty: "pages") var diary: LinkingObjects<Diary>
    
    convenience init(
        title: String,
        body: String,
        createdDate: Date = .now,
        isMarked: Bool = false
    ) {
        self.init()
        self.title = title
        self.body = body
        self.createdDate = createdDate
        self.isMarked = isMarked
    }
}

Realmオブジェクトの伝搬

environment(_:_:)モディフィアを使用して下位のSceneViewにconfigurationを共有することができます。特に指定しない場合、フェッチなどではdefaultConfigurationが使用されます。Realm自体を指定する方法もあるようです。

import SwiftUI
import RealmSwift

@main
struct DiaryApp: SwiftUI.App {
    var body: some Scene {
        WindowGroup {
	    MainView()
                .environment(\.realmConfiguration, .init(...))
		//または
		.environment(\.realm, .init(...))
	}
    }
}

各種プロパティラッパー

@Persisted

オブジェクトを宣言する際にプロパティに付与します。オブジェクトの定義を参照してください。

@ObservedResults

Realmからオブジェクトをフェッチして、自動で変更を監視して反映してくれるものです(Core Dataでいうところの@FetchRequest)。イニシャライザでconfigurationを特に指定しない場合、内部のRealmではEnvironmentValueで渡されたrealmConfigurationが使用されます。$記号をつけてアクセスすることで、filtersortDescriptorを設定できるようです。その他の使用法についてはViewでRealmオブジェクトの情報を表示するを参照してください。

struct DiaryList: View {
    @ObservedResults(Diary.self) var diaries
    ...
    var body: some View {
        List(diaries) { diary in
	    ...
	}
    }
}

// 呼び出し
DiaryList()

@ObservedRealmObject

ObjectまたはRealmSwift.Listの変更を監視してViewに反映させるプロパティラッパーです(@ObservedObjectのようなもの)。
テキストフィールドなどにBindingを渡すときは$page.titleのように簡単に記述できます。内部的にrealm.write { ... }のようなトランザクションを扱っているので、UIからオブジェクトに変更を加えるとすぐにRealmに反映されます。

struct PageEditor: View {
    @ObservedRealmObject var page: Page
    ...
    var body: some View {
        TextField("タイトル", text: $page.title)
	...
    }
}

// 呼び出し
let page: Page
PageEditor(page: page)

ViewでRealmオブジェクトの情報を表示する

単一の場合

@ObservedRealmObjectプロパティラッパーを使用します。wrappedValueの中のプロパティのバインディングを作る際には、SwiftUI.Stateなどと同様に先頭に$記号をつけます。簡単ですね。

@ObservedRealmObject var page: Page
// そのまま渡す場合
page.title // String
// バインディングを渡す場合
$page.title // Binding<String>

複数の場合

複数のRealmオブジェクトをSwiftUIで扱う方法は2つあります。一つはResults、もう一つはRealmSwift.Listです。対応するプロパティラッパーは次の通りです。

wrappedValue プロパティラッパー 生成元
Results ObservedResults 現在のView
RealmSwift.List ObservedRealmObject 上の階層のView

Results

@ObservedResults(Page.self) var pages // Viewのinitの引数にならない

pages // Results<Page>
$pages // ObservedResults<Page>

ResultsSwift.RandomAccessCollectionに準拠しているので、SwiftUI.ListForEachで扱うことができます。

さらに、ObservedResultsRealmSwift.BoundCollectionというプロトコルにも準拠しています。このプロトコルは、書き込みトランザクションに関する記述を省略できるという便利機能を備えています。つまり下記のように書けます。

@Environment(\.realm) var realm

let newPage = Page(title: "New Page", body: "")

// 便利機能なしver
realm.thaw().write {
    diary.append(newPage)
}

// 便利機能ありver
$diary.pages.append(newPage)

ソースコードを見る限り、BoundCollectionは内部的に書き込みトランザクションを開いているようです。このほかにもremovemoveなどのメソッドも用意されています。

ObservedResultsfilterについては後述します。

List

こちらもSwift.RandomAccessCollectionに準拠しているので、ListやForEachで扱うことができます。

@ObservedRealmObject var pages: RealmSwift.List<Page> // Viewのinitの引数となる

pages // RealmSwift.List<Page>
$pages // ObservedRealmObject<List<Page>>.Wrapper(あまり使わない)

検索、フィルター

プレビューデータの扱い方

この例ではプレビューデータをひとつのスコープにまとめるためにenumを使用します。

enum PreviewData {
    static var realm: Realm {
        let realm = try! Realm(configuration: .init(inMemoryIdentifier: "PreviewData.realm"))
        
        try! realm.write {
            realm.deleteAll()
        }
        
        try! realm.write {
            Self.diaries.forEach { diary in
                realm.add(diary)
            }
        }
        return realm
    }
}

どのRealmにも紐づけられていない(管理されていない)オブジェクトをプレビューで使用するのは危険なので、staticなrealmプロパティからしかプレビューデータにアクセスできないように、生のオブジェクトのアクセスレベルをfileprivateに制限しています。

extension PreviewData {
    fileprivate static var diaries: [Diary] {
        [
	    ...
        ]
    }
}

Preview側ではこのようにアクセスします。

struct PageEditor_Previews: PreviewProvider {
    static var previews: some View {
        let page = PreviewData.realm.objects(Page.self).first!
        PageEditor(page: page)
    }
}




後書き

バージョン

RealmSwift: version 10.20.2
Xcode: version 13.2

参考記事

https://docs.mongodb.com/realm/sdk/swift/swiftui/
https://qiita.com/ttttpzm/items/f29229fb063b8f7e1920
https://zenn.dev/yuto05490/scraps/bc8509c7f34375
https://zenn.dev/men_so/articles/02e89dbb561fd4
https://s-cape.dev/realmのconfiguringで何が設定できるのか/

Discussion