Sponsor Link
環境&対象
- 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 を継承したクラスで、モデル定義する必要があり、以下のようになります。
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)
}
}
- Realm では、UUID を型としてサポートしていないので、uuidString メソッドで String 化した値を保持することにします。
- UUID は、ユニークなので、primaryKey に使うよう設定します。
- Realm は、reflection で情報を取得するので、init の実装が必要です。
- 通常の init です
RealmTODOItem から 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 を変更しなくて済みます)
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
}
}
- CoreData 用 ViewModel と同じインターフェースでメソッドを用意します
- 内部向けに メソッドを用意します
- CoreData 用 ViewModel と同じインターフェースでメソッドを用意します
- Realm は、NSSortDescriptor を扱えないので、Realm の扱うことのできる SortDescriptor に変換して使います
- CoreData 用 TODOItemStore と同じインターフェースでメソッドを用意します
- CoreData 用 TODOItemStore と同じインターフェースでメソッドを用意します
- Realm を fetch するケースが多かったので、別メソッドを作りました
- CoreData 用 TODOItemStore と同じインターフェースでメソッドを用意します
- ViewModel は、NotificationCenter からの Notification.Name.NSManagedObjectContextObjectsDidChange 通知を受けて更新しているので、Realm モデル更新時にも同じ 通知を送っています。(もう少し改善する余地がありそうです)
- CoreData 用 TODOItemStore と同じインターフェースでメソッドを用意します
- CoreData 用 TODOItemStore と同じインターフェースでメソッドを用意します
- CoreData 用 TODOItemStore と同じインターフェースでメソッドを用意します
- Realm では、Realm の要素プロパティを偏向時に反映されるため、save は不要です
NSSortDescriptor と SortDescriptor の対応
上記でも説明していますが、Realm は、NSSortDescriptor は使わずに、独自の SortDescriptor を使ってソートを行います。
その変換のために、以下の Extention も作成しました
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 をサポートする仕組みを持っていないので、自前で実装する必要があります。
それまでは、非対応ということで、以下の空メソッドを実装します。
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 を入れ替えることが確認できました。
- Realm では、UUID に対応していないので、uuidString を使って、String として保存
- id を PrimaryKey に設定すると良い
- Realm は、NSSortDescriptor に対応していないので、SortDescriptor に変換して使用する
- Realm では、リフレクションで情報を取得するため 引数なしの init 実装が必要
- MVVM で作成していれば、View に提供された情報は、直接のモデル情報ではないので、freeze を使う必要はなかった
説明は以上です。
不明な点やおかしな点ありましたら、こちらまで。
おすすめの Realm 本
Web で API 等を Reference で確認することができますが、じっくりと腰を据えて学習したいときには、学習の順番が考慮されている本を使うのがおすすめです。
Realm については、本がほとんど出版されていませんが、以下の本は おすすめです。
Sponsor Link