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 のチェックも兼ねてます
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 を受け取れるようにします。
func filteredItems(_ predicate: NSPredicate? = nil,_ sortDescs: [NSSortDescriptor] = []) -> [TODOItem] {
var items:[TODOItem] = []
let request:NSFetchRequest = 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 の詳細ページに遷移してテストしています。
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 に追加する必要があります。
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 部分のみ抜粋します
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 のリストを受け取れるよう設定します。
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