1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Posted at

はじめに

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

記載すること

SwiftUIにおいてRealmの変更を検知しUIを自動更新させる方法

実装方法

Realmの変更監視を行う上で重要になるのが以下2点です。

  1. NotificationToken
  2. 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)
}

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?