Sponsor Link
環境&対象
- maxOS Catalina 10.15.7
- Xcode 12.3
- iOS 14.2
ソートの追加
TODOItem に Priority を追加したので、リストのソートも、タイトル順だけでなく、プライオリティ順のソートも追加してみます。
ソート実装方針
Model が ソート順(NSSortDescriptor) を受け取り、ソート済み配列を返しているので、Model に渡す NSSortDescriptor を 外部から調整すれば良いことになります。
ViewModel で 適切な NSSortDescriptor を管理し、Model に渡すことにします。
テストは、Model を対象にしたものと、View を対象にしたもので行います。
ソート順を指定する UI は、Toolbar を使ってみます。
ソートの実装
Model のテスト
CoreData を疑うわけではないですが、以下のようなテストを作成しました。
# 自分で作成する NSSortDescriptor のチェックも兼ねてます
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
func test_variousSort_PriorityOrName_sortAccordingly() throws { let model = TODOItemStore(true) // (1) let prioSort = NSSortDescriptor(key: "priority", ascending: false) let titleSort = NSSortDescriptor(key: "title", ascending: true) // (2) _ = model.createTODOItem("Middle", "Middle", false, .middle) _ = model.createTODOItem("Low", "Low", false, .low) _ = model.createTODOItem("Highest", "Highest", false, .highest) // (3) let sortedWithPrio = model.filteredItems(nil, [prioSort, titleSort]) XCTAssertEqual(sortedWithPrio[0].title, "Highest") XCTAssertEqual(sortedWithPrio[1].title, "Middle") XCTAssertEqual(sortedWithPrio[2].title, "Low") // (4) let sortedWithTitle = model.filteredItems(nil, [titleSort, prioSort]) XCTAssertEqual(sortedWithTitle[0].title, "Highest") XCTAssertEqual(sortedWithTitle[1].title, "Low") XCTAssertEqual(sortedWithTitle[2].title, "Middle") } |
- NSSortDescriptor を準備
- 複数のプライオリティでモデルを作成
- Title でソートして順番をテスト
- Priority でソートして順番をテスト
Model の実装
Priority 順ソートの時にも2番目のソート種類として Title を渡したいので、Model の filteredItems の引数として、複数の NSSortDescriptor を受け取れるようにします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
func filteredItems(_ predicate: NSPredicate? = nil,_ sortDescs: [NSSortDescriptor] = []) -> [TODOItem] { var items:[TODOItem] = [] let request:NSFetchRequest<CDTODOItem> = CDTODOItem.fetchRequest() if let predicate = predicate { request.predicate = predicate } if sortDescs.count > 0 { request.sortDescriptors = sortDescs } do { items = try container.viewContext.fetch(request).map(TODOItem.init) } catch { TODOItemStore.logger.error("error in fetching data from coredata") } return items } |
上記のコードで、Model のテストコードはパスするようになります。
View の テスト
以下のようなテストを作成しました。 プライオリティ順だけでなく、タイトル順でも適切にソートされるかをテストします。
リスト上の表示を取得できないため、個別 Item の詳細ページに遷移してテストしています。
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 |
func test_variousSort_nameSortOrPrioSort_shouldBeSortedAccordingly() throws { let app = XCUIApplication() app.launchArguments.append("TestWithInMemory") app.launch() let mainPage = TODOListPageObject(app) XCTAssertEqual(mainPage.todoListRows.count, 0) // add sleep(1) mainPage.addButtonTap().typeTitle("0MiddleItem").typeDetail("MiddleDetail").selectPriority(2).tapOk() sleep(1) mainPage.addButtonTap().typeTitle("1LowItem").typeDetail("LowDetail").selectPriority(1).tapOk() sleep(1) mainPage.addButtonTap().typeTitle("2HighestItem").typeDetail("HighestDetail").selectPriority(4).tapOk() // check Title sort _ = mainPage.changeToTitleSort() // check top item var topItemDetailPage = mainPage.rowPageObjectAtIndex(at: 0).tapToDetailVew() sleep(1) XCTAssertEqual(topItemDetailPage.titleText, "0MiddleItem") topItemDetailPage.tapOk() var bottomItemDetailPage = mainPage.rowPageObjectAtIndex(at: 2).tapToDetailVew() sleep(1) XCTAssertEqual(bottomItemDetailPage.titleText, "2HighestItem") bottomItemDetailPage.tapOk() // check Priority sort _ = mainPage.changeToPrioritySort() topItemDetailPage = mainPage.rowPageObjectAtIndex(at: 0).tapToDetailVew() sleep(1) XCTAssertEqual(topItemDetailPage.titleText, "2HighestItem") topItemDetailPage.tapOk() bottomItemDetailPage = mainPage.rowPageObjectAtIndex(at: 2).tapToDetailVew() sleep(1) XCTAssertEqual(bottomItemDetailPage.titleText, "1LowItem") bottomItemDetailPage.tapOk() } |
View の実装を行う前に、テストコードを整備する必要があります。
具体的には、Toolbar に追加するソートボタンを TODOListPageObject に追加する必要があります。
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 |
class TODOListPageObject: PageObject { var app: XCUIApplication init(_ app: XCUIApplication) { self.app = app } private var todoList: XCUIElement { app.tables["TODOList"]} var todoListRows:XCUIElementQuery { todoList.cells } private var navBar: XCUIElement { app.navigationBars["MyTODO"]} private var addButton: XCUIElement { navBar.buttons.element(boundBy: 1) } private var editButton: XCUIElement { navBar.buttons.element(boundBy: 0) } private var doneButton: XCUIElement { navBar.buttons.element(boundBy: 0) } private var toggleButton: XCUIElement { navBar.buttons.element(boundBy: 2)} // (1) private var titleSortButton: XCUIElement { app.buttons["Title"]} private var prioSortButton: XCUIElement { app.buttons["Priority"]} func addButtonTap() -> TODOListItemDetailPageObject { addButton.tap() return TODOListItemDetailPageObject(app) } // (2) func changeToTitleSort() -> TODOListPageObject { titleSortButton.tap() return self } // (3) func changeToPrioritySort() -> TODOListPageObject { prioSortButton.tap() return self } func removeItemWithEdit(index: Int) -> TODOListPageObject { editButton.tap() todoListRows.element(boundBy: index).buttons["Delete "].tap() todoListRows.element(boundBy: index).buttons["Delete"].tap() doneButton.tap() return self } func removeItemWithSwipe(index:Int) -> TODOListPageObject { todoListRows.element(boundBy: index).swipeLeft() todoListRows.element(boundBy: index).buttons["Delete"].tap() return self } func rowPageObjectAtIndex(at: Int) -> TODOItemRowPageObject { return TODOItemRowPageObject(self.app, cell: todoListRows.element(boundBy: at)) } func toggleFilter() -> TODOListPageObject { toggleButton.tap() return self } } |
- タイトル順ソートボタン、プライオリティ順ソートボタンを取得
- タイトル順ソートボタン押下メソッド
- プライオリティ順ソートボタン押下メソッド
これで、View のテストも完成しました。
View/ViewModel の実装
View としては、MyTODOMainView にソートを指定するボタンを追加します。
MyTODOMainView が大きくなってきたので、Toolbar 部分のみ抜粋します
1 2 3 4 5 6 7 8 |
ToolbarItemGroup(placement: .bottomBar) { Button(action: viewModel.changeToTitleFirstSort, label: { Text("Title")}) // .accessibility(identifier: "TitleSortButton") Button(action: viewModel.changeToPrioFirstSort, label: { Text("Priority")}) // .accessibility(identifier: "PrioSortButton") } |

なお、現時点では、Toolbar に配置した時の AccessibilityID で UI 要素の取得はできませんでした。
ViewModel は、ソート指定用の NSSortDescriptor を保持し、View から指定されたソート方法を使って、Model から TODOItem のリストを受け取れるよう設定します。
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 |
class MyTODOViewModel : ObservableObject { var todoItemStore: TODOItemStore @Published var todoItems:[TODOItem] = [] @Published var showUndoneItems:Bool = true var predicate: NSPredicate { if showUndoneItems { return NSPredicate(format: "isDone == false") } return NSPredicate(format: "isDone == true") } var sortDescs:[NSSortDescriptor] = [NSSortDescriptor(key: "title", ascending: true), NSSortDescriptor(key: "priority", ascending: false)] // .. snip .. func changeToPrioFirstSort() { self.sortDescs = [NSSortDescriptor(key: "priority", ascending: false), NSSortDescriptor(key: "title", ascending: true)] updateTodoItems(nil) } func toggleDisplayFilter() { showUndoneItems.toggle() updateTodoItems(nil) } // .. snip .. } |
今回の実装で、タイトルだけでなく、プライオリティ順に表示することもできるようになりました。
説明は以上です。
不明な点やおかしな点ありましたら、こちらまで。
Sponsor Link