はじめに
SwiftUIでアプリを作成しており、データ管理にRealmを利用しています。
Realmの変更が行われた際に変更を検知して自動でビューを更新させる実装を行なったのでその方法を記載します。
記載すること
SwiftUIにおいてRealmの変更を検知しUIを自動更新させる方法
実装方法
Realmの変更監視を行う上で重要になるのが以下2点です。
- NotificationToken
- Realm.observe
NotificationTokenとは
RealmにおけるNotificationTokenの役割
概要 | 説明 |
---|---|
Realmの変更を監視する | データの追加・変更・削除をリアルタイムで監視する。 |
コールバック | データ変更を検知して自動的に指定した処理を実行する。 |
監視停止 | 監視を明示的に停止できる。(メモリリーク防止)。 超大事なので必ず実施すること。 |
Realmの変更を監視し、変更を検知したら通知が送られてくる。
↓
通知が送られてくると、自動的に指定した処理を実行する。
↓
処理実行後、アプリにリアルタイム反映する。(ex. リスト最新化、画面再描画など)
Realm.observeとは
Realmの変更を検知するため仕組み
オブジェクトを追加した、変更した、削除を検知して通知を送ってくれる。
変更を検知すると changes
という形で内容を通知してくれる。
changes
には以下の種類がある。
初回表示の時、変更された時、エラーがあった時に応じて処理内容を変更できる。
.initial
: 初回表示時
.update
: オブジェクト更新時
.error
: エラー発生時
以上の内容を踏まえて実装を見ていきましょう。
TodoEntityの変更を監視するマネージャークラス
このクラスではインスタンス生成時に observeRealm()
を呼び出して変更監視設定を入れています。
変更を検知するとfeachTodos()
が呼び出され最新の情報をTodoEntityから取得し以下のデータを変数に保持しています。
・カテゴリごとのデータ
・カテゴリごとの件数
import Foundation
import RealmSwift
class TodoManager: ObservableObject {
@Published var count: Int = 0
@Published var todos: [TodoEntity] = []
private var notificationToken: NotificationToken?
private var category: TodoEntity.Category
private var realm: Realm
init(category: TodoEntity.Category) {
realm = try! Realm()
self.category = category
observeRealm()
}
// Realmの変更を監視
private func observeRealm() {
let results = realm.objects(TodoEntity.self)
// result.observeでTodoEntityの変更を検知
notificationToken = results.observe { [weak self] changes in
// observeでTodoEntityの変更内容によって個別に処理設定
switch changes {
// .initial:初期読み込み時
// .update: 更新時(追加・変更・削除)
case .initial, .update:
self?.feachTodos()
case .error(let error):
print("Realm observe error: \(error)")
}
}
}
// データ取得
private func feachTodos() {
let results = realm.objects(TodoEntity.self).filter("category == %@", category.rawValue)
self.count = results.count
self.todos = Array(results)
}
// 通知の停止
deinit {
// インスタンスが破棄される時に必ず呼び出す(★超重要)
notificationToken?.invalidate()
}
}
上記実装を見ていてinit、deinitってなんだっけ?と思った方は参考までにこちらを確認してください。
【SwiftUI】initとdeinitについて
補足
@ObservedRealmObjectについて
ObservedRealmObjectでもRealmの変更を検知することは可能です。
では、その場合NotificationTokenっていらないのでは?と思いますよね。
結論からいうと単一Entityのデータ表示のようなシンプルな実装であれば@ObservedRealmObjectで対応できるので不要です。
ただし、もっと細かい監視やカスタム動作が必要な時はObservedRealmObjectでは対応できないので自前で設定したほうがいい。
サンプル実装
最後に上記の説明を踏まえたサンプル実装を記載します。
サンプル実装の内容は以下の通りです。
・TodoEntityの内容をリスト表示する。
・TodoEntityに新たにデータ追加した場合に自動的にリストが更新される。
エンティティクラス
import RealmSwift
class TodoEntity: Object, Identifiable {
@objc dynamic var id: ObjectId = ObjectId.generate()
@objc dynamic var category: Int16 = 0
@objc dynamic var state: Int16 = 0
@objc dynamic var task: String?
@objc dynamic var time:Date?
override static func primaryKey() -> String? {
"id"
}
enum Category: Int16 {
case ImpUrg_1st
case ImpUrg_2st
func toString() -> String {
switch self {
case .ImpUrg_1st:
return "XXXX"
case .ImpUrg_2st:
return "YYYY"
}
}
}
// MARK: データ登録
func create(category: TodoEntity.Category, task: String, time:Date?) {
let realm = try! Realm()
let todoEntity = TodoEntity()
do {
todoEntity.id = ObjectId.generate()
todoEntity.category = category.rawValue
todoEntity.state = State.todo.rawValue
todoEntity.task = task
todoEntity.time = time
try realm.write {
realm.add(todoEntity)
print("Import Success")
}
} catch {
print("Import Error:\(error)")
}
}
}
TodoEntityの変更を監視するマネージャークラス
import Foundation
import RealmSwift
class TodoManager: ObservableObject {
@Published var count: Int = 0
@Published var todos: [TodoEntity] = []
private var notificationToken: NotificationToken?
private var category: TodoEntity.Category
private var realm: Realm
init(category: TodoEntity.Category) {
realm = try! Realm()
self.category = category
observeRealm()
}
// Realmの変更を監視
private func observeRealm() {
let results = realm.objects(TodoEntity.self)
notificationToken = results.observe { [weak self] changes in
switch changes {
case .initial, .update:
self?.feachTodos()
case .error(let error):
print("Realm observe error: \(error)")
}
}
}
// データ取得
private func feachTodos() {
let results = realm.objects(TodoEntity.self).filter("category == %@", category.rawValue)
self.count = results.count
self.todos = Array(results)
}
deinit {
notificationToken?.invalidate()
}
}
Realmのデータをリスト表示
TodoEntityに登録されているデータをリストで画面表示します。
import SwiftUI
import RealmSwift
struct TodoList: View {
var category: TodoEntity.Category
@StateObject private var todoManager: TodoManager
@State var newTask: String = ""
init(category: TodoEntity.Category) {
self.category = category
_todoManager = StateObject(wrappedValue: TodoManager(category: category))
}
var body: some View {
VStack {
List {
ForEach(0..<todoManager.count, id: \.self) { i in
Text(todoManager.todos[i].task!)
}
}
}
HStack {
TextField("新しいタスク", text: $newTask)
.foregroundColor(.secondary)
.textFieldStyle(RoundedBorderTextFieldStyle())
Button {
self.addNewTask()
} label: {
Text("保存")
.foregroundStyle(Color.tBlue)
}
Button {
self.clearTask()
} label: {
Text("クリア").foregroundStyle(Color.tRed)
}
}
}
private func addNewTask() {
TodoEntity.create(category: category, task: newTask, time: nil)
clearTask()
}
private func clearTask() {
newTask = ""
}
}
#Preview {
let category = TodoEntity.Category.ImpUrg_1st
TodoList(category: category)
}