[SwiftUI][CoreData] SwiftUI と MVVM で始める CoreData 入門 (その4:ViewModel の実装)

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

環境&対象

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

  • macOS Catalina 10.15.7
  • Xcode 12.2
  • iOS 14.2

View/ViewModel の実装

以下の UI を想定しています。

  • TODOItem をリスト表示する(表示は、タイトル、詳細)
  • 右上の + を押したら、新しい要素が作成される
  • 行を左スワイプ or Edit ボタン押下後削除ボタンを押すと、該当要素が削除される

View の実装から使われる ViewModel の実装

View は、ViewModel 経由で Model を操作するので、ViewModel に上記の操作に必要な メソッドが必要となります。

  • 表示用の TODOItem の配列
  • TODOItem を作成
  • (指定した)TODOItem の削除

ViewModel のテスト追加

上記のメソッド向けのテストを作ります。

ViewModel テスト向けに、新しいファイルを追加しました。

ViewModel_Tests

//
//  ViewModel_Tests.swift
//
//  Created by : Tomoaki Yagishita on 2020/12/12
//  © 2020  SmallDeskSoftware
//

import XCTest
@testable import MyTODO

class ViewModel_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.
    }
    // (1)
    func test_initialize_atLaunch_shouldNotFail_ZeroRows() throws {
        let viewModel = MyTODOViewModel(true) // in-memory core-data
        XCTAssertNotNil(viewModel)
        
        XCTAssertEqual(viewModel.todoItems.count, 0)
    }
    // (2)
    func test_addNewItems_twoItems_shouldBeAppearedInTODOItems() throws {
        let viewModel = MyTODOViewModel(true) // in-memory core-data

        _ = viewModel.createTODOItem("Item1", "Item1Detail")
        XCTAssertEqual(viewModel.todoItems.count, 1)

        _ = viewModel.createTODOItem("Item2", "Item2Detail")
        XCTAssertEqual(viewModel.todoItems.count, 2)
        
        let item1 = try XCTUnwrap(viewModel.todoItems.first(where: {$0.title == "Item1"}))
        XCTAssertEqual(item1.title,"Item1")
        XCTAssertEqual(item1.detail,"Item1Detail")

        let item2 = try XCTUnwrap(viewModel.todoItems.first(where: {$0.title == "Item2"}))
        XCTAssertEqual(item2.title,"Item2")
        XCTAssertEqual(item2.detail,"Item2Detail")
    }
    // (3)
    func test_addAndRemove_oneItemInTheEnd_shouldBeAppearedInTODOItems() throws {
        let viewModel = MyTODOViewModel(true)
        
        let item1 = viewModel.createTODOItem("Item1", "Item1Detail")
        let _ = viewModel.createTODOItem("Item2", "Item2Detail")
        let item3 = viewModel.createTODOItem("Item3", "Item3Detail")
        XCTAssertEqual(viewModel.todoItems.count, 3)
        
        viewModel.deleteTODOItem(item1)
        XCTAssertEqual(viewModel.todoItems.count, 2)
        XCTAssertNil(viewModel.todoItems.first(where: {$0.title == "Item1"}))
        XCTAssertNotNil(viewModel.todoItems.first(where: {$0.title == "Item2"}))
        XCTAssertNotNil(viewModel.todoItems.first(where: {$0.title == "Item3"}))

        viewModel.deleteTODOItem(item3)
        XCTAssertEqual(viewModel.todoItems.count, 1)
        XCTAssertNil(viewModel.todoItems.first(where: {$0.title == "Item1"}))
        XCTAssertNotNil(viewModel.todoItems.first(where: {$0.title == "Item2"}))
        XCTAssertNil(viewModel.todoItems.first(where: {$0.title == "Item3"}))
    }
}
コード解説
  1. in-memory 設定で作成して、初期の todoItems 配列が 空であることをテストしています
  2. 要素を作成し、配列が適宜増えていること、追加された要素が指定した値のプロパティを持っていることをテストしています
  3. 追加後に削除し、残っているべき要素が残っていることを確認しています

なお、実装はまだしていないので、この時点でコンパイルはできません。

ViewModel の実装

View 用に、todoItems という配列を、@Published 指定で持ち、作成と削除のメソッドを追加しました。

todoItems は、何か操作したらアップデートするようにしています。(Observer や Publisher を使うことも考えましたが、最初はシンプルに作りました)

MyTODOViewModel code

//
//  MyTODOViewModel.swift
//
//  Created by : Tomoaki Yagishita on 2020/12/10
//  © 2020  SmallDeskSoftware
//

import Foundation
import SwiftUI
import CoreData
import os
import Combine

class MyTODOViewModel : ObservableObject {
    static let logger = Logger(subsystem: "com.smalldesksoftware.MyTODO.MyTODOViewModel", category: "MyTODOViewModel")

    var todoItemStore: TODOItemStore
    @Published var todoItems:[TODOItem] = []
    
    
    init(_ inMemory: Bool) {
        self.todoItemStore = TODOItemStore(inMemory)
        todoItems = todoItemStore.items
    }

    func createTODOItem(_ title: String, _ detail: String = "") -> TODOItem{
        let newItem = todoItemStore.createTODOItem(title, detail)
        todoItems = todoItemStore.items
        return newItem
    }
    
    func deleteTODOItem(_ item: TODOItem) {
        todoItemStore.removeTODOItem(item)
        todoItems = todoItemStore.items
    }
}
作成・削除ともに、Model(MyTODOModel) のメソッドを呼び出し todoItems をアップデートするという実装になっています。

テスト

上記の実装で、先に定義した テストをすべてパスすることが確認できます。

View で使われる ViewModel のメソッドが準備できたので、次こそ、View を実装します。

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

コメントを残す

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