[SwiftUI][CoreData] SwiftUI と MVVM で始める CoreData 入門 (その14:CoreData を Realm と入れ替え)

SwiftUI と CoreData を組み合わせたアプリの作り方を説明します。

環境&対象

以下の環境で動作確認を行なっています。

  • maxOS Catalina 10.15.7
  • Xcode 12.3
  • iOS 14.2

CoreData を Realm に入れ替える

MVVM(Model-View-ViewModel) で作ってきました。Model 入れ替えも簡単にできるハズなので、CoreData を Realm に入れ替えてみます。

CoreData を Realm に置き換える方針

View/ViewModel にできるだけ変更を入れないで対応してみます。

CoreData を Realm に置き換え時に課題になりそうな点

ざっくりとは、CoreData と Realm の相違点をチェックせずに作った MVVM なので、どこまで共有できるか不明です

以下は、少し調べて判明した 対応課題 です。

Model アップデート通知
現在は、NotificationCenter の NSManagedobjectContext 依存の通知を使っています
NSSortDescriptor
Realm は、独自の SortDescriptor を使います (NSSortDescriptor 自体は、Foundation に含まれるクラスです)
freeze
SwiftUI では、リスト等の表示に使われる配列は、変更されない(immutable) である必要があります。Realm には、そのために Results 型に freeze メソッドが用意されています

上記の点にも留意しながら修正していきます。

CoreData の Realm への入れ替え作業

ターゲット追加

まずは、新しい iOS App ターゲットを追加します。

名前は、"MyRealmTODO" としました。

Realm を SwiftPM で追加

SwiftPM 経由で Realm を追加します。

[File]-[Swift Packages]-[Add Package Dependencies...] 経由で、RealmSwift の URL "https://github.com/realm/realm-cocoa" を指定します。

追加対象のターゲットの確認ウィンドウで、先ほど作成したターゲット "MyRealmTODO" を選択します。(Realm, RealmSwift 共に指定します)

Realm 用 TODOItem 定義

Realm では、Object を継承したクラスで、モデル定義する必要があり、以下のようになります。

RealmTODOItem

class RealmTODOItem: Object {
    // (1)
    @objc dynamic var id = ""  // UUID().uuidstring instead of UUID
    @objc dynamic var title = ""
    @objc dynamic var detail = ""
    @objc dynamic var isDone = false
    @objc dynamic var priority:Int16 = 2
    // (2)
    override static func primaryKey() -> String? {
        return "id"
    }
    // (3)
    override init() {
        id = UUID().uuidString
    }
    // (4)
    init(id: UUID, title: String, detail: String, isDone: Bool, priority: TODOItem.Priority) {
        self.id = id.uuidString
        self.title = title
        self.detail = detail
        self.isDone = isDone
        self.priority = Int16(priority.rawValue)
    }
}
コード解説
  1. Realm では、UUID を型としてサポートしていないので、uuidString メソッドで String 化した値を保持することにします。
  2. UUID は、ユニークなので、primaryKey に使うよう設定します。
  3. Realm は、reflection で情報を取得するので、init の実装が必要です。
  4. 通常の init です

RealmTODOItem から TODOItem を作る init も定義します。

TODOItem.init

extension TODOItem {
    init(_ realmItem: RealmTODOItem) {
        self.id = UUID(uuidString: realmItem.id)
        self.title = realmItem.title
        self.detail = realmItem.detail
        self.isDone = realmItem.isDone
        self.priority = Priority.init(value: realmItem.priority)
    }
}

Realm 用 TODOItemStore の定義

実際には、TODOItemStore が DB layer とのやりとりを行いますので、ほとんど書き直しとなります。

CoreData 用の TODOItemStore は、Realm 用のターゲットには含めませんので、同じ名称で作成します。(ViewModel を変更しなくて済みます)

TODOItemStore

struct TODOItemStore : TODOItemStoreProtocol {
    var configuration: Realm.Configuration
    // (1)    
    init(_ inMemory: Bool) {
        if inMemory {
            configuration = Realm.Configuration(inMemoryIdentifier: "MyInMemoryRealm")
        } else {
            configuration = Realm.Configuration()
        }
    }
    // (2)
    private var realm: Realm {
        return try! Realm(configuration: configuration)
    }
    // (3)
    var items: [TODOItem] {
        return realm.objects(RealmTODOItem.self).map(TODOItem.init)
    }
    // (4)
    func convertNSSortDescriptorToSortDescriptor(_ nsSortDescs: [NSSortDescriptor]) -> [SortDescriptor] {
        var sortDescs:[SortDescriptor] = []
        for nsSortDesc in nsSortDescs {
            if let sortDesc = nsSortDesc.sortDescriptor {
                sortDescs.append(sortDesc)
            }
        }
        return sortDescs
    }
    // (5)
    func filteredItems(_ predicate: NSPredicate?, _ nsSortDescs: [NSSortDescriptor]) -> [TODOItem] {
        let sortDescs = convertNSSortDescriptorToSortDescriptor(nsSortDescs)
        if let predicate = predicate {
            return realm.objects(RealmTODOItem.self).filter(predicate).sorted(by: sortDescs).map(TODOItem.init)
        }
        return realm.objects(RealmTODOItem.self).sorted(by: sortDescs).map(TODOItem.init)
    }
    // (6)
    func refetchItem(_ id: UUID) -> TODOItem? {
        guard let realmitem = fetchRealmItem(id) else { return nil }
        return TODOItem.init(realmitem)
    }
    
    // (7)
    func fetchRealmItem(_ id: UUID) -> RealmTODOItem? {
        let fetchResult = realm.objects(RealmTODOItem.self).filter("id == %@", id.uuidString)
        if fetchResult.count == 1 { return fetchResult.first}
        return nil
    }
    // (8)
    func createTODOItem(_ title: String, _ detail: String, _ isDone: Bool, _ priority: TODOItem.Priority) -> TODOItem {
        let realmItem = RealmTODOItem(id: UUID(), title: title, detail: detail, isDone: isDone, priority: priority)
        try! realm.write{
            realm.add(realmItem)
        }
        // (9)
        NotificationCenter.default.post(name: Notification.Name.NSManagedObjectContextObjectsDidChange, object: nil)
        return TODOItem(realmItem)
    }
    // (10)
    func updateItem(_ id: UUID, title: String?, detail: String?, isDone: Bool?, priority: TODOItem.Priority?) -> TODOItem? {
        guard let realmItem = fetchRealmItem(id) else { return nil }
        try! realm.write {
            if let title = title {
                realmItem.title = title
            }
            if let detail = detail {
                realmItem.detail = detail
            }
            if let isDone = isDone {
                realmItem.isDone = isDone
            }
            if let priority = priority {
                realmItem.priority = Int16(priority.rawValue)
            }
        }
        NotificationCenter.default.post(name: Notification.Name.NSManagedObjectContextObjectsDidChange, object: nil)
        return TODOItem(realmItem)
    }
    // (11)
    func removeTODOItem(_ item: TODOItem) {
        guard let realmItem = fetchRealmItem(item.id!) else { return }
        try! realm.write {
            realm.delete(realmItem)
        }
        NotificationCenter.default.post(name: Notification.Name.NSManagedObjectContextObjectsDidChange, object: nil)
    }
    // (12)
    func toggleIsDone(_ item: TODOItem) {
        guard let id = item.id else { return }
        _ = self.updateItem(id, title: item.title, detail: item.detail, isDone: item.isDone, priority: item.priority)
    }
    // (13)
    func save() {
        // nothing to do
    }
}
コード解説
  1. CoreData 用 ViewModel と同じインターフェースでメソッドを用意します
  2. 内部向けに メソッドを用意します
  3. CoreData 用 ViewModel と同じインターフェースでメソッドを用意します
  4. Realm は、NSSortDescriptor を扱えないので、Realm の扱うことのできる SortDescriptor に変換して使います
  5. CoreData 用 TODOItemStore と同じインターフェースでメソッドを用意します
  6. CoreData 用 TODOItemStore と同じインターフェースでメソッドを用意します
  7. Realm を fetch するケースが多かったので、別メソッドを作りました
  8. CoreData 用 TODOItemStore と同じインターフェースでメソッドを用意します
  9. ViewModel は、NotificationCenter からの Notification.Name.NSManagedObjectContextObjectsDidChange 通知を受けて更新しているので、Realm モデル更新時にも同じ 通知を送っています。(もう少し改善する余地がありそうです)
  10. CoreData 用 TODOItemStore と同じインターフェースでメソッドを用意します
  11. CoreData 用 TODOItemStore と同じインターフェースでメソッドを用意します
  12. CoreData 用 TODOItemStore と同じインターフェースでメソッドを用意します
  13. Realm では、Realm の要素プロパティを偏向時に反映されるため、save は不要です

NSSortDescriptor と SortDescriptor の対応

上記でも説明していますが、Realm は、NSSortDescriptor は使わずに、独自の SortDescriptor を使ってソートを行います。

その変換のために、以下の Extention も作成しました

NSSortDescriptor extension for supporting SortDescriptor

extension NSSortDescriptor {
    var sortDescriptor:SortDescriptor? {
        guard let key = self.key else { return nil }
        let sortDesc = SortDescriptor(keyPath: key, ascending: self.ascending)
        return sortDesc
    }
}

Realm は、Undo/Redo をサポートする仕組みを持っていないので、自前で実装する必要があります。

それまでは、非対応ということで、以下の空メソッドを実装します。

TODOItemStore extension for UNDO/REDO

extension TODOItemStore {
    func undo() {
        print("not implemented")
    }
    func redo() {
        print("not implemented")
    }
    var canUndo:Bool {
        return false
    }
    var canRedo:Bool {
        return false
    }
}

上記のように実装することで、ViewModel, View だけではなく、App も共用することができました。

まとめ:CoreData を Realm と入れ替える時の懸念点とその対応

MVVM で作ると、非常に簡単に DB Layer を入れ替えることが確認できました。

CoreData を Realm と入れ替える時の懸念点とその対応
  • Realm では、UUID に対応していないので、uuidString を使って、String として保存
  • id を PrimaryKey に設定すると良い
  • Realm は、NSSortDescriptor に対応していないので、SortDescriptor に変換して使用する
  • Realm では、リフレクションで情報を取得するため 引数なしの init 実装が必要
  • MVVM で作成していれば、View に提供された情報は、直接のモデル情報ではないので、freeze を使う必要はなかった

説明は以上です。
不明な点やおかしな点ありましたら、こちらまで。

おすすめの Realm 本

Web で API 等を Reference で確認することができますが、じっくりと腰を据えて学習したいときには、学習の順番が考慮されている本を使うのがおすすめです。

Realm については、本がほとんど出版されていませんが、以下の本は おすすめです。

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です