SwiftUIでのRealmの使い方まとめ(プロパティラッパー、フェッチ、プレビュー、etc.)
はじめに
この記事は、Realm初心者の僕がSwiftUIでRealmを使うにあたってつまずいたところを主にまとめた物です。新たな知見があれば追記していくつもりです。間違いなどありましたら気軽にコメント欄でご指摘ください。
プロジェクトの例として、DiaryとPageというオブジェクトで構成される日記アプリを説明で用います。Diaryは複数のPageを内包する、というぐらいの認識で大丈夫です。
導入方法(XcodeプロジェクトでSwiftPM)
Xcodeのサイドバーのプロジェクト -> PROJECT -> Package Dependencies -> "+"ボタンをクリック -> サーチフィールドに"https://github.com/realm/realm-swift"を入力 -> 右下の"Add Package"ボタンをクリック
オブジェクトの定義
Realmで扱いたいオブジェクトは、Object
を継承したclass
として宣言します。それぞれのプロパティには@Persisted
というプロパティラッパーを付与します。
プロパティタイプは、Bool
、Int
、Int8
、Int16
、 Int32
、Int64
、Double
、Float
、String
、Date
、Data
がサポートされています。
オブジェクトを識別するためのプライマリーキーには引数のprimaryKey
をtrue
とします。ここでは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(_:_:)
モディフィアを使用して下位のScene
やView
に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
が使用されます。$
記号をつけてアクセスすることで、filter
やsortDescriptor
を設定できるようです。その他の使用法については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>
Results
はSwift.RandomAccessCollection
に準拠しているので、SwiftUI.List
やForEach
で扱うことができます。
さらに、ObservedResults
はRealmSwift.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は内部的に書き込みトランザクションを開いているようです。このほかにもremove
やmove
などのメソッドも用意されています。
ObservedResults
のfilter
については後述します。
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
参考記事
Discussion