【SwiftUI】Realmの変更を検知してUIを自動更新させる

### はじめに
SwiftUIでアプリを作成しており、データ管理にRealmを利用しています。
Realmの変更が行われた際に変更を検知して自動でビューを更新させる実装を行なったのでその方法を記載します。

### 記載すること
SwiftUIにおいてRealmの変更を検知しUIを自動更新させる方法

## 実装方法
Realmの変更監視を行う上で重要になるのが以下2点です。
1. NotificationToken
2. Realm.observe

### NotificationTokenとは
RealmにおけるNotificationTokenの役割
| 概要 | 説明 |
|:-:|:-:|
| Realmの変更を監視する  | データの追加・変更・削除をリアルタイムで監視する。  |
| コールバック  | データ変更を検知して自動的に指定した処理を実行する。|
| 監視停止  | 監視を明示的に停止できる。（メモリリーク防止）。<br> 超大事なので必ず実施すること。|

Realmの変更を監視し、変更を検知したら通知が送られてくる。
↓
通知が送られてくると、自動的に指定した処理を実行する。
↓
処理実行後、アプリにリアルタイム反映する。（ex. リスト最新化、画面再描画など）

### Realm.observeとは
Realmの変更を検知するため仕組み
オブジェクトを追加した、変更した、削除を検知して通知を送ってくれる。
変更を検知すると `changes` という形で内容を通知してくれる。
`changes` には以下の種類がある。
初回表示の時、変更された時、エラーがあった時に応じて処理内容を変更できる。
`.initial` : 初回表示時
`.update` : オブジェクト更新時
`.error` : エラー発生時

以上の内容を踏まえて実装を見ていきましょう。

#### TodoEntityの変更を監視するマネージャークラス
このクラスではインスタンス生成時に `observeRealm()` を呼び出して変更監視設定を入れています。
変更を検知すると`feachTodos()`が呼び出され最新の情報をTodoEntityから取得し以下のデータを変数に保持しています。
・カテゴリごとのデータ
・カテゴリごとの件数

``` Swift
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について](https://qiita.com/goDAc2015/items/0ee93c3222d0c964cfe7)

### 補足
@ObservedRealmObjectについて
ObservedRealmObjectでもRealmの変更を検知することは可能です。
では、その場合NotificationTokenっていらないのでは？と思いますよね。

結論からいうと単一Entityのデータ表示のようなシンプルな実装であれば@ObservedRealmObjectで対応できるので不要です。
ただし、もっと細かい監視やカスタム動作が必要な時はObservedRealmObjectでは対応できないので自前で設定したほうがいい。

## サンプル実装
最後に上記の説明を踏まえたサンプル実装を記載します。
サンプル実装の内容は以下の通りです。
・TodoEntityの内容をリスト表示する。
・TodoEntityに新たにデータ追加した場合に自動的にリストが更新される。

#### エンティティクラス
``` Swift
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の変更を監視するマネージャークラス
``` Swift
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に登録されているデータをリストで画面表示します。
``` Swift
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)
}

```