Sponsor Link
環境&対象
- macOS Catalina 10.15.7
- Xcode 12.2
- iOS 14.2
Xcode からテンプレートして作成されるコードは、MVVM とは言い難いので、MVVM アーキテクチャになるよう修正していきます。
MVVM 視点で修正したい箇所
MVVM では、View は、ViewModel のみを知っていることが前提ですが、テンプレートで生成されたコードには、ContentView に、CoreData が直接出てくるので、NG です。
Environment に、ManagedObjectContext を設定できるようにしている時点で、Apple としては、CoreData を Model Layer に留めておく気はないのかもしれません。
修正していく方針
- View は ViewModel 経由で Model にアクセスする
- Model は、基本ロジック等の CoreData に依存しない部分と、保存等の CoreData に依存する部分を意識して分離する
- @Published 等の仕組みで変更を伝播させることとして、@FetchRequest は当面使用しない
特に2つ目の点は、将来的に、モデルレイヤーの入れ替え、例えば、CoreData からRealm に切り替えようとした時に、作り直しになってしまうかどうかに大きな影響を与えます。
モデルを作る(TODOItem)
TODO のアプリなので、以下のプロパティを持つ要素を作ることにします。
- ID (id:UUID)
- タイトル (title:String)
- 詳細 (detail:String)
Model も CoreData に非依存で作成したいので、Swift 的なモデルと CoreData 上のモデルの2つを作成します。
Swift 的な struct を、TODOItem とし、 CoreData 上の Entity を CDTODOItem としました。(CD = CoreData)
struct TODOItem {
var id: UUID? = nil
var title: String = ""
var detail: String = ""
let logger = Logger(subsystem: "com.smalldesksoftware.MyTODO.TODOItem", category: "TODOItem")
init(_ title: String,_ detail: String = "") {
self.title = title
self.detail = detail
}
}
モデルを作る(TODOItemStore)
TODOItem を束ねて保持する要素が欲しくなるので、TODOItemStore とします。
できれば、TODOItemStore も CoreData 非依存としたいのですが、Model を CoreData 非依存とすると、ViewModel で Model と CoreData を管理することになってしまいます。
ViewModel を CoreData 非依存にしたいので、TODOItemStore だけは、CoreData へのリファレンスを持つこととしました。
Model の中で、DB layer 含めて完結する形になります。
// depend on CoreData
struct TODOItemStore {
var items:[TODOItem] = []
let container: NSPersistentContainer
let logger = Logger(subsystem: "com.smalldesksoftware.MyTODO.TODOItemStore", category: "TODOItemStore")
}
PersistentContainer から要素を読み込むために TODOItem.init と TODOItemStore.init を作ります。
// MARK: CoreData dependent part
extension TODOItem {
init(_ cdItem: CDTODOItem) {
self.id = cdItem.id
self.title = cdItem.title ?? ""
self.detail = cdItem.detail ?? ""
}
}
// depend on CoreData
struct TODOItemStore {
var items:[TODOItem] = []
let container: NSPersistentContainer
let logger = Logger(subsystem: "com.smalldesksoftware.MyTODO.TODOItemStore", category: "TODOItemStore")
init(_ inMemory:Bool ) {
container = NSPersistentContainer(name: "MyTODO")
if inMemory {
container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
}
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
fetchFromCoreData()
}
mutating func fetchFromCoreData() {
let request:NSFetchRequest = CDTODOItem.fetchRequest()
do {
items = try container.viewContext.fetch(request).map(TODOItem.init)
} catch {
logger.error("error in fetching data from coredata")
}
}
}
ここまでで、モデルに対して、コンパイルできる環境は作れたので、Model に対して、基本機能の実装を始めます。
TDD で Model の実装をすすめる
MyTODOApp.swift や ContentView.swift を変更する前に、モデルに機能を実装していきます。
現時点では、モデルの定義のみ存在し、機能する API がありません。
要素作成テストの実装
まずは、要素を作成し Model に保存できるかを確認するテストを作ります。
Model のテスト用に、Model_Tests.swift を、MyTODOTest フォルダ下に作成します。
TODOItem を作成できることを確認する test_createItem_newItem_CorrectValues を以下のように書きました。
//
// Model_Tests.swift
//
// Created by : Tomoaki Yagishita on 2020/12/10
// © 2020 SmallDeskSoftware
//
import XCTest
@testable import MyTODO
class Model_Tests: XCTestCase {
override func setUpWithError() throws {
// Put setup code here. This method is called before the invocation of each test method in the class.
}
override func tearDownWithError() throws {
// Put teardown code here. This method is called after the invocation of each test method in the class.
}
func test_createItem_newItem_withCorrectValues() {
// prep model with coredata
var model = TODOItemStore(true)
XCTAssertEqual(model.items.count, 0)
// create new item
model.createTODOItem("Item Title", "item detail")
XCTAssertEqual(model.items.count, 1)
// get item from model
let item = model.items.first!
// test : compare properties
XCTAssertEqual(item.title, "Item Title")
XCTAssertEqual(item.detail, "item detail")
}
}
メソッド定義すらしていないので、当然コンパイルエラーです。
TODOItemStore.createTODOItem 実装
TODOItem を作るメソッドを以下のように実装しました。
extension TODOItemStore {
// create Item , then put into coredata
mutating func createTODOItem(_ title: String, _ detail: String = "") {
let newItem = TODOItem(title, detail)
let newCDItem = CDTODOItem(context: container.viewContext)
newCDItem.id = newItem.id
newCDItem.title = newItem.title
newCDItem.detail = newItem.detail
items.append(newItem)
save()
}
func save() {
if !container.viewContext.hasChanges { return }
do {
try container.viewContext.save()
} catch {
print(error)
}
}
}
上記のコードを追加すると、コンパイルできるようになり、テストもパスします。
現時点では、オンメモリの CoreData を使ってテストしているので、きちんと CoreData に保存されたかをテストする方法がありませんが、後日、追加することを検討します。
要素削除テストの実装
削除のテストも作ってみます。
func test_removeItem_existingItem_shouldBeVanished() {
var model = TODOItemStore(true)
XCTAssertEqual(model.items.count, 0)
// create new item
model.createTODOItem("Item Title", "item detail")
XCTAssertEqual(model.items.count, 1)
// get item from model
let item = model.items.first!
model.removeTODOItem(item)
XCTAssertEqual(model.items.count, 0)
}
要素削除の実装
削除するメソッドは、以下のように実装しました。
mutating func removeTODOItem(_ item: TODOItem) {
guard let id = item.id else { return }
guard let index = items.firstIndex(where: {$0.id == item.id}) else { return }
let request:NSFetchRequest = CDTODOItem.fetchRequest()
request.predicate = NSPredicate.init(format: "id == %@", id as CVarArg)
let deleteRequest = NSBatchDeleteRequest.init(fetchRequest: request)
do {
try self.container.viewContext.execute(deleteRequest)
} catch {
print(error)
}
items.remove(at: index)
save()
}
上記の実装で、テストはパスします。
ここまでの実装で、プログラム的には、TODOItem を作成し、削除することができるようになりました。
次回は、UI を使ったテストとその実装を行っていきます。
まとめ:Xcode12.2 で CoreData プロジェクト:CoreData は、M(Model) に入れてしまうのが良い
- テンプレートは、MVVM になっていないので、修正が必要
- CoreData は、Model 内に留めておくと、綺麗な MVVM を作りやすい
- CoreData に依存する Model を限定的にしておくと、再利用しやすい
説明は以上です。
不明な点やおかしな点ありましたら、こちらまで。
Sponsor Link