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)
1 2 3 4 5 6 7 8 9 10 11 12 13 |
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 } } |

CDTODOItem 定義
モデルを作る(TODOItemStore)
TODOItem を束ねて保持する要素が欲しくなるので、TODOItemStore とします。
できれば、TODOItemStore も CoreData 非依存としたいのですが、Model を CoreData 非依存とすると、ViewModel で Model と CoreData を管理することになってしまいます。
ViewModel を CoreData 非依存にしたいので、TODOItemStore だけは、CoreData へのリファレンスを持つこととしました。
Model の中で、DB layer 含めて完結する形になります。
1 2 3 4 5 6 7 8 9 |
// 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 を作ります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
// 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 を以下のように書きました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
// // 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 を作るメソッドを以下のように実装しました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
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 に保存されたかをテストする方法がありませんが、後日、追加することを検討します。
要素削除テストの実装
削除のテストも作ってみます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
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) } |
要素削除の実装
削除するメソッドは、以下のように実装しました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
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