【SwiftUI】Realmの@ObservedResultsの使い方!値の観測と解凍

この記事からわかること
- Swiftでデータを永続的に
保存 する方法 - Realmで値の変更を観測する方法
- @ObservedResultsの使い方
- configurationの指定
- コレクションの操作方法
- 凍結したRealmの意味とthawメソッド
index
[open]
\ アプリをリリースしました /
iOSアプリのデータを永続的に保存する目的で使用されるデータベース「Realm(レルム)」で値の変更を観測する@ObservedResultsの使い方をまとめていきます。
Realmの使い方や導入方法に関しては以下の記事を参考にしてください。
参考文献: React to Changes - SwiftUI
@ObservedResults:クエリ結果を観測
公式リファレンス:ObservedResults - SwiftUI
@ObservedResults
はRealm Swiftライブラリで使用できるプロパティーラッパーの1つです。
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
@propertyWrapper public struct ObservedResults: DynamicProperty, BoundCollection where ResultType: _ObservedResultsValue & RealmFetchable & KeypathSortable & Identifiable {
}
@ObservedResults
を使用することでRealmデータベースに格納されているオブジェクト(レコード)のコレクションを観測することができます。付与したプロパティからデータベースのオブジェクトに参照できるので中からデータを取り出すことが可能です。
objects()
メソッドを使用した結果と同じ以下のようなResults<Element>
形式で取得できます。
Results<User> <0x13ec05050> (
[0] User {
id = EEE309AB-0482-4CE7-8D70-9DF6BAC03F9C;
name = ame;
age = 20;
}
}
使い方
使用するには@ObservedResults
の引数に取り出したいテーブルクラスを指定します。
struct ContentView: View {
@ObservedResults(User.self) var users
var body: some View {
List(users) { user in
Text(user.name)
}.listStyle(GroupedListStyle())
}
}
configurationを指定する
@ObservedResults
構造体は以下のようなイニシャライザを持っているのでRealm.Configuration
を明示的に設定することも可能です。
public init(_ type: ResultType.Type,
configuration: Realm.Configuration? = nil,
where: ((Query<ResultType>) -> Query<Bool>)? = nil,
keyPaths: [String]? = nil,
sortDescriptor: SortDescriptor? = nil) where ResultType: Object
例えばテーブルクラスのプロパティを追加し、マイグレーションを行うときは以下のように@ObservedResults
のイニシャライザからschemaVersion
を指定することができます。
struct ContentView: View {
@ObservedResults(User.self,configuration: Realm.Configuration(schemaVersion: 1)) var users
var body: some View {
List(users) { user in
Text(user.name)
}.listStyle(GroupedListStyle())
}
}
親ビューからrealmConfigurationを引き継ぐ
@ObservedResults
を子ビューで使用している場合は親ビューから設定を引き継ぐことも可能です。
ChildView().environment(\.realm, try! .init(configuration: Realm.Configuration()))
取得するデータにフィルターをかける
引数where
に条件を渡すことで取得するオブジェクトにフィルターをかけることも可能です。
struct ContentView: View {
@ObservedResults(User.self,where: {$0.age < 10}) var users
var body: some View {
List(users) { user in
Text(user.name)
}.listStyle(GroupedListStyle())
}
}
取得するデータにソートをかける
引数sortDescriptor
にSortDescriptor
構造体型の条件を渡すことで取得するデータにソートをかけることができます。
keyPath
にはプロパティ名をascending
には昇順であればtrue
を降順であればfalse
を渡します。
struct ContentView: View {
@ObservedResults(User.self,sortDescriptor:SortDescriptor(keyPath: "age", ascending: false)) var users
var body: some View {
List(users) { user in
Text(user.name)
}.listStyle(GroupedListStyle())
}
}
コレクションの操作
@ObservedResults
で参照できる変数に$
を付与することでコレクションの操作も可能になります。これはRealmSwift.BoundCollection
プロトコルの持つ特徴で内部的にwriteトランザクションが実行されておりwrite
の記述が省略可能になります。
例えば以下のようにUserを削除する処理(#1)やUserを追加ボタン(#2)がコレクション操作を行うだけで実装可能になっています。
struct ContentView: View {
@ObservedResults(User.self) var users
var body: some View {
List {
ForEach(users) { user in
Text(user.name)
}.onDelete(perform: $users.remove) // (#1)
}.listStyle(GroupedListStyle())
// (#2)
Button(action: {
let user = User()
user.name = "ame"
user.age = 20
$users.append(user)
}, label: {
Text("User追加")
})
}
}
onDeleteでコレクションを削除
コレクションを削除する時はonDelete
モディファイアを使用して以下のように記述することでList表示している1行単位でスワイプすることでデータを削除できます。
.onDelete(perform: $users.remove) // (#1)
上記の記法だとこの処理しか記述できないので引数にindex
を受け取るクロージャにすることで別の処理も混ぜることができるようになります。
.onDelete(perform: { index in
$users.remove(atOffsets: index)
// 別の処理
})
Realmを解凍して使う
逆にwrite
メソッドを自分で呼び出して使う場合は注意が必要です。デフォルトでは@ObservedResults
は凍結しており、writeメソッドを使って処理を実行しようとするとエラーになってしまいます。
エラーが発生するコード
struct ContentView: View {
@ObservedResults(User.self) var users
var body: some View {
List {
ForEach(users) { user in
HStack{
Text(user.name)
Text("\(user.age)")
}.onTapGesture {
try! user.realm!.write{
user.age = 80
}
}
}.onDelete(perform: $users.remove)
}.listStyle(GroupedListStyle())
}
}
エラー内容
Thread 1: "Can't perform transactions on a frozen Realm"
// スレッド 1: 「凍結されたレルムでトランザクションを実行できません」
thawメソッド
これを解決するには凍結されたRealmを使用できるように解凍します。解凍するにはthaw
メソッドを使います。これでwrite
メソッドが使用可能になります。またfreeze
メソッドを使用することで凍結させることも可能です。
struct ContentView: View {
@ObservedResults(User.self) var users
var body: some View {
List {
ForEach(users) { user in
HStack{
Text(user.name)
Text("\(user.age)")
}.onTapGesture {
let thawUser = user.thaw()
try! thawUser?.realm!.write{
thawUser?.age = 80
}
// thawUser?.freeze()
}
}.onDelete(perform: $users.remove)
}.listStyle(GroupedListStyle())
}
}
まだまだ勉強中ですので間違っている点や至らぬ点がありましたら教えていただけると助かります。
ご覧いただきありがとうございました。
私がSwift UI学習に使用した参考書
まだまだ勉強中ですので間違っている点や至らぬ点がありましたら教えていただけると助かります。
ご覧いただきありがとうございました。