ObservedResults (opens new window)

ObservedResultsとは超簡単に説明すると SwiftUI の List や Form で RealmSwift のオブジェクトを扱うために作られたラッパープロパティのこと。

というのも、SwiftUI と RealmSwift のライフサイクルのタイミングの違いの問題で、RealmSwift.ListRealmSwift.Resultsの結果を List や Form で表示して、それを編集しようとするとバグが発生してしまっていました。

そのために@ObservableObjectfreezeでごにょごにょしなきゃいけなかったのですが、それら全てから開放されるのがこの@ObservedResultsになります。

価格

基本的な使い方

RealmSwift のドキュメントに載っている通りに解説しようと思います。

ちなみに、以下の記事を最初に読んでおくと幸せになれます。

RealmCocoa が SwiftUI に正式対応してるっぽい (opens new window)

RealmCocoa がまたアップデートしてるんだが (opens new window)

Person クラス

10.10.0 のアップデートで@Persistedが推奨になり、@objc dynamic varRealmOptionalなどは全て利用する必要がなくなった。

import RealmSwift
import SwiftUI
class Person: Object {
    @Persisted(primaryKey: true) var _id: String
    @Persisted var name: String
    @Persisted var age: Int
}
1
2
3
4
5
6
7
8

ContentView

作成されたPersonクラスの結果であるRealmSwift.Results<Person>を List で表示する。

従来の方法

表示するだけなら現在でもこの方法が利用できる。

ただし、Realm はインスタンスに変化があるとその更新の通知が即座に反映されてしまうので、onMoveonDeleteを実装するとクラッシュしてしまう。

struct ContentView: View {
    // 事前にどこかで`realm`を宣言しておくこと
    @State var persons: realm.objects(Person.self)
    var body: some View {
        List {
            ForEach(persons) { person in
                Text(person.name)
            }
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12

ObservedObject を利用した方法

import RealmSwift
class Persons: ObservableObject {
    @Published var persons: RealmSwift.Results<Person> = realm.objects(Person.self)
}
1
2
3
4
5

まず最初に上のようにObservableObjectを定義しておき、

struct ContentView: View {
    @ObservedObject var persons: Persons
    var body: some View {
        List {
            ForEach(persons.persons) { person in
                Text(person.name)
            }
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11

という風に利用する。ただしこれも結局編集しようとすると落ちてしまうので意味がない。

編集しても落ちないようにするためにはfreezeしたオブジェクトを List に渡す必要がある。

ObservedResults を利用した方法

freezeを利用する方法などは学ばずに、バカ正直に Realm 謹製の@OservedResultsを利用するのが良い。

struct ContentView: View {
    @ObservedResults(Person.self) var persons
    var body: some View {
        List {
            ForEach(persons) { person in
                Text(person.name)
            }
            .onMove(perform: $persons.move)
            .onDelete(perform: $persons.remove)
        }.navigationBarItems(trailing:
            Button("Add") {
                $persons.append(Person())
            }
        )
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

これだけで全く落ちない完璧なコードが書ける。

フィルタリングやソート

ここで注意しなければいけないのは@ObservedResultsは中身がfreezeしたオブジェクトであるので、List 等で表示するのは便利だが扱い方が少し異なるという点である。

List として表示するときにソートしたりフィルタリングしたりする方法が異なるので覚えておきたい。

公式ドキュメントでは省略されているが、以下が正しい@ObservedResultsの宣言方法である。

@ObservedResults(Person.self, filter: NSPredicate(format: "age >= 20"), sortDescriptor: SortDescriptor(keyPath: "age", ascending: false)) var persons
1

更に詳しく述べると

実はこれに加えて更にconfigurationを使って RealmSwift のConfigurationを設定することも可能である。

が、今回はそこまでは利用しないと考えて割愛した。

フィルタリング

NSPredicateを利用してフィルタリングをすることができる。利用方法は概ね普通にRealmSwift.Resultsに対してフィルタリングする場合と同じなのだが、ちょっと違うところもあるので書いておく。

比較演算子

// 従来
@State var persons = realm.objects(Person.self).filter("age >= %@", 20)
// NSPredicate
@ObservedResults(Person.self, filter: NSPredicate(format: "age >= %@", argumentArray: [20]))
1
2
3
4
5

ただのイコール判定をするだけなら比較的わかりやすいのですが、INがミスしやすいです。

// 従来
@State var persons = realm.objects(Person.self).filter("age IN %@", [20, 24, 30])
// NSPredicate
@ObservedResults(Person.self, filter: NSPredicate(format: "age IN %@", argumentArray: [[20, 24, 30]]))
1
2
3
4
5

なお、これらを詳しくまとめた記事がSwift で Realm を使う時の Tips(3) NSPredicate 編 (opens new window)で公開されていますので、器になる方はぜひ読んで見てください。

ソート

ソートにはSortDescriptorを利用します。

// 従来
@State var persons = realm.objects(Person.self).sorted(byKeyPath: "age")
// SortDescriptor
@ObservedResults(Person.self, sortDescriptor: SortDescriptor(keyPath: "age"))
1
2
3
4
5

一応、従来の方法との組み合わせで、

struct ContentView: View {
    @ObservedResults(Person.self) var persons
    var body: some View {
        List {
            ForEach(persons.sorted(byKeyPath: "age")) { person in
                Text(person.name)
            }
            .onMove(perform: $persons.move)
            .onDelete(perform: $persons.remove)
        }.navigationBarItems(trailing:
            Button("Add") {
                $persons.append(Person())
            }
        )
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

みたいな書き方もできますが、@ObservedResultsfreezeでありそのままではリアルタイム更新されないので、これをやるとObservedResultsと ForEach の中身の ID がズレるので削除したのとは違うカラムが消えてしまいます。

なのでこの書き方は避けるようにしましょう。

記事は以上。

価格