Sponsor Link
目次
環境&対象
- macOS Catalina 10.15.7
- Xcode 12.2
- iOS 14.2
ViewModel の準備ができたところで、View を作っていきます。
View の実装
View のテストを作成する
View を実装するにあたり、View のテストから作り始めます。
View も in-memory でテストする
Model, ViewModel ともに、in-memory の CoreData でテストしました。
それまでに残っているデータを考慮せずにテストできるので、テストは、in-memory の方がやりやすいです。
Model テスト、ViewModel テスト どちらも API ベースでのテストなので、in-memory の指定が行いやすかったのですが、アプリ経由でのテストになると、引数指定して initializer を呼べません。
アプリの引数指定で、アプリ起動時に、in-memory の指定ができるようにすると便利です。

View(App) の実装 (初期化時に、in-memory を選択できるように)
テストでの実行時に、”TestWithInMemory” と引数に指定することで、InMemory の CoreData を使うようにします。
そのために、App を以下のようにしました。
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 |
// // MyTODOApp.swift // // Created by : Tomoaki Yagishita on 2020/12/10 // © 2020 SmallDeskSoftware // import SwiftUI import os @main struct MyTODOApp: App { static let logger = Logger(subsystem: "com.smalldesksoftware.MyTODO.MyTODOApp", category: "MyTODOApp") @StateObject var viewModel: MyTODOViewModel init() { // (1) let inMemory:Bool = ProcessInfo.processInfo.arguments.contains("TestWithInMemory") // (2) _viewModel = StateObject(wrappedValue: MyTODOViewModel(inMemory)) } var body: some Scene { WindowGroup { ContentView() .environmentObject(viewModel) } } } |
- アプリ起動時の引数は、ProcessInfo.processInfo.arguments 経由で取得できます
- StateObject をこのように初期化することもできます
こうすることで、起動時の引数指定で、CoreData を In-Memory にすることができます。
View のテストコード
In-Memory で起動できるようになったので、View のテストコードを書いていきます。
まずは、In-Memory で起動して、List が空であり ボタン等が存在することをテストします。
UITest ように新しいファイル を作成しました。(ターゲットは、UITest です)
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 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 |
// // View_Tests.swift // // Created by : Tomoaki Yagishita on 2020/12/12 // © 2020 SmallDeskSoftware // import XCTest class ViewTests_UITests: XCTestCase { override func setUpWithError() throws { // Put setup code here. This method is called before the invocation of each test method in the class. // In UI tests it is usually best to stop immediately when a failure occurs. continueAfterFailure = false // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. } override func tearDownWithError() throws { // Put teardown code here. This method is called after the invocation of each test method in the class. } func test_launch_noData_EmptyList() throws { let app = XCUIApplication() // (1) app.launchArguments.append("TestWithInMemory") app.launch() // (2) let todoList = app.tables["TODOList"] XCTAssertTrue(todoList.exists) // (3) let listRows = todoList.cells XCTAssertEqual(listRows.count, 0) // (4) let navBar = app.navigationBars["MyTODO"] XCTAssertTrue(navBar.exists) // (5) let addButton = navBar.buttons.element(boundBy: 1) XCTAssertTrue(addButton.exists) let editButton = navBar.buttons.element(boundBy: 0) XCTAssertTrue(editButton.exists) } func test_addAndRemoveItem_afterLaunch_todoItemsShouldBeUpdated() throws { let app = XCUIApplication() app.launchArguments.append("TestWithInMemory") app.launch() let todoList = app.tables["TODOList"] let listRows = todoList.cells XCTAssertEqual(listRows.count, 0) let navBar = app.navigationBars["MyTODO"] let addButton = navBar.buttons.element(boundBy: 1) let editButton = navBar.buttons.element(boundBy: 0) // (6) // add addButton.tap() XCTAssertEqual(listRows.count, 1) addButton.tap() XCTAssertEqual(listRows.count, 2) // (7) // remove editButton.tap() listRows.element(boundBy: 1).buttons["Delete "].tap() listRows.element(boundBy: 1).buttons["Delete"].tap() XCTAssertEqual(listRows.count, 1) } } |
- TestWithInMemory という引数を指定して、アプリを起動します
- TODOList という Accessibility ID が付与された List を取得します
- List に含まれる 行 を 取得します
- List に含まれる行が 0 であることをテストします
- NavigationBar に Edit ボタンと 追加ボタン があることをテストします
- 追加ボタンを2回押下し、リストが2行になることをテストします
- Edit ボタンを押下し、行に表示される削除ボタンを押して要素を削除、リストの行が1行になっていることをテストします
まだ、View を修正していないため、エラーとなります。
View の実装
まずは、以下の機能を実装します。
- TODO item をリスト表示
- TODO item を追加
- TODO item を削除 (ForEach の .onDelete を利用)
以下のようなコードになります。
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 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 |
// // ContentView.swift // // Created by : Tomoaki Yagishita on 2020/12/10 // © 2020 SmallDeskSoftware // import SwiftUI import CoreData struct ContentView: View { @EnvironmentObject var viewModel: MyTODOViewModel var body: some View { NavigationView { List { // (1) ForEach(viewModel.todoItems, id:\.self) { item in Text("\(item.id?.uuidString ?? "noID") \(item.title)") } // (2) .onDelete(perform: deleteItems) } // (3) .accessibility(identifier: "TODOList") // (3) .navigationTitle("MyTODO") // (4) .toolbar { #if os(iOS) ToolbarItem(placement: .navigationBarLeading) { EditButton() } #endif ToolbarItem(placement: .navigationBarTrailing) { Button(action: addItem) { Label("Add Item", systemImage: "plus") } } } } } // (5) private func addItem() { withAnimation { _ = viewModel.createTODOItem("NewItem", "NewDetail") } } // (6) private func deleteItems(offsets: IndexSet) { withAnimation { offsets.map { viewModel.todoItems[$0] }.forEach(viewModel.deleteTODOItem) } } } struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView().environmentObject(MyTODOViewModel(false)) } } |
- ForEach を使って、TODOItem を表示します。Title と ID を表示しています
- 削除する機能は、SwiftUI の提供する onDelete を使います
- UITest からアクセスできるように Accessibility ID を設定します
- Toolbar に、EditButton と 追加するボタンを設定しています。
- 追加ボタンが押された時に、ViewModel の該当メソッドを呼びます
- 削除するときには、ViewModel の該当メソッドを呼びます
シミュレータで動かしてみると、以下のようになります。
テスト
この View の実装を使うと、先ほど作成したテストをパスすることがわかります。
ここまでの振り返り
これで、非常にシンプルな View の実装はできたことになります。
以下の点を改良します。
- TODOItem 表示の改良(タイトル、詳細を表示)
- TODOItem 追加時に、タイトル等を指定できるようにする
- TODOItem に、完了フラグを追加する
いろいろと考えられると思いますが、まずは上記の点を改良していきます。
一度、コードのリファクタリングをしてから、上記の点を改良していきます。
説明は以上です。
不明な点やおかしな点ありましたら、こちらまで。
Sponsor Link