

どんなモデルとビューを作ったかは、上記記事を参照ください。
Sponsor Link
アプリ設計:振り返り
ここまでに、MVVM の基本的なところを実装し、以下ができるようになっています。
- ボーリングスコア風の表示
- 1〜10フレームのスコア記録
初期の設計で困るところは発生していませんので、このまま実装していきます。
今回は、「スコア計算」です。
アプリ設計:追加設計
スペアとストライクの扱いはまだ着手せずに進めていきます。
つまり、スコア計算は、それまでの各投球でのピン数を合計すれば良いことになります。
ただし、以下を決定しておかないといけません。
- 投球が完了していないフレームでの合計スコア表示
1投しただけの状態で、その1投目までの合計を表示すべきか、2投目が終わるまで、”-” 表示で良いかを決める必要があります。
ここでは、2投目が終わるまでは(スペア、ストライクを考慮した時には、必要な情報が集められるようになるまでは)、スコア表示しないこととして、代わりに”-” を表示することとしました。
もしかするとボーリングのルールとしては、途中での計算は決めていないのかもしれません。
スコア計算もチェックするテスト
前回のテストを改良して、第1フレームの2投を入力後にスコア表示をチェックするように修正しましょう。
まずは、テストでチェックできるように Accessibility ID を付与する必要があります。
ここでは、FrameScoreView0 〜 FrameScoreView9 という ID を付与するようにしています。
1 2 3 4 5 6 7 8 9 10 11 12 |
struct FrameScoreView: View { @ObservedObject var viewModel: BowlingGameViewModel let frameIndex: Int var body: some View { Text(viewModel.scoreAsText(frame: frameIndex)) .accessibility(identifier: "FrameScoreView\(frameIndex)") .frame(width: 50, height: 20) .border(Color.gray.opacity(0.5)) } } |
以下は、2投した状態で、チェックするテストです。
1投目は1ピン、2投目は5ピン倒した結果を入力として、表示内容をチェックしています。
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 |
func test_RecordOneFrame_TwoBowl_ShouldBeRecordedAndAtFirstFrame() throws { // UI tests must launch the application that they test. let app = XCUIApplication() app.launch() let button1 = app.buttons["Button1"] XCTAssertTrue(button1.exists) let button5 = app.buttons["Button5"] XCTAssertTrue(button5.exists) let frame0bowl0Label = app.staticTexts["FrameBowlView0-0"] XCTAssertTrue(frame0bowl0Label.exists) let frame0bowl1Label = app.staticTexts["FrameBowlView0-1"] XCTAssertTrue(frame0bowl1Label.exists) let frame0ScoreLabel = app.staticTexts["FrameScoreView0"] XCTAssertTrue(frame0ScoreLabel.exists) XCTAssertEqual(frame0bowl0Label.label, "-") XCTAssertEqual(frame0bowl1Label.label, "-") XCTAssertEqual(frame0ScoreLabel.label, "-") button1.tap() XCTAssertEqual(frame0bowl0Label.label, "1") XCTAssertEqual(frame0bowl1Label.label, "-") XCTAssertEqual(frame0ScoreLabel.label, "-") button5.tap() XCTAssertEqual(frame0bowl0Label.label, "1") XCTAssertEqual(frame0bowl1Label.label, "5") XCTAssertEqual(frame0ScoreLabel.label, "6") } |
setupWithError 内の、continueAfterFailure を = true とすると、Assert が Failure しても最後までテストを実行するようになります。
そうすることで、各投球の表示は期待通りになっていて、スコア表示のテストで Failure していることがわかります。
ここまでのスコア計算の実装状況
Model ではなにも実装していません。以下のように、ViewModel が、”0″ を返しているという実装です。ですので、初期状態でも、テストが Failure しています。
1 2 3 4 5 |
func scoreAsText(frame:Int) -> String { return "0" } |
ここでは、ViewModel が計算するのではなく、Model に問い合わせて結果を View に渡すという実装にします。
スコア計算の実装
BowlingGame へ実装
モデル (BowlingGame) へスコア計算を実装していきます。
スペア・ストライクを考慮しないのであれば、フレーム分の2投後でのスコアは、
「前のフレームまでのスコア+2投で倒したピン数」となります。
また、関数返り値を Int? として、まだ計算するための情報が揃っていない時には、nil を返す仕様とします。
1 2 3 4 5 6 7 8 9 10 11 12 |
func frameResult(frame: Int) -> Int? { if let lastResult = frame == 0 ? 0 : frameResult(frame: frame - 1) { if let bowl0 = bowlResult(frame: frame, bowl: 0) { if let bowl1 = bowlResult(frame: frame, bowl: 1) { return lastResult + bowl0 + bowl1 } } } return nil } |
ViewModel は、Model の計算結果を使って、View に返すように実装を修正します。
1 2 3 4 5 6 7 8 |
func scoreAsText(frame:Int) -> String { if let result = game.frameResult(frame: frame) { return String(result) } return "-" } |
先ほどの1フレームのテストは、うまく Pass します。
シミュレータでの実行は、以下のようになります。
スペア・ストライクをチェックしていないので、入力された数字の合計がスコアになっています。
いまは、1フレームしかテストしていませんので、(スペア・ストライクでない)10フレームまでの入力で、スコアが正しいことを確認するテストを追加しましょう。
Refactoring
テストを追加する前に、将来的なテストに対しての準備を兼ねて、すこし Refactoring しておきましょう。
ボタンとテキストは、常に参照すると思いますので、Setup で取得して、配列で保持しておくことにします。
テキストが存在するかを確認するテストも別途作成しました。(test_LabelExists)
ボタンをテキストを事前に取得しておくことで、これまでのテスト test_RecordOneFrame_TwoBowl_ShouldBeRecordedAndAtFirstFrame は、シンプルになりました。
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 |
// // TDDBowlingUITests.swift // // Created by : Tomoaki Yagishita on 2020/11/08 // © 2020 SmallDeskSoftware // import XCTest class TDDBowlingUITests: XCTestCase { var bowlButtons: [XCUIElement] = [] var frameBowlLabels:[[XCUIElement]] = [[XCUIElement]]() var frameScoreLabels: [XCUIElement] = [] var totalScoreLabel: XCUIElement! override func setUpWithError() throws { // In UI tests it is usually best to stop immediately when a failure occurs. continueAfterFailure = true let app = XCUIApplication() app.launch() for index in 0..<10 { let button = app.buttons["Button\(index)"] bowlButtons.append(button) let bowl0 = app.staticTexts["FrameBowlView\(index)-0"] let bowl1 = app.staticTexts["FrameBowlView\(index)-1"] frameBowlLabels.append([bowl0, bowl1]) let score = app.staticTexts["FrameScoreView\(index)"] frameScoreLabels.append(score) } totalScoreLabel = app.staticTexts["TotalScoreView"] } override func tearDownWithError() throws { // Put teardown code here. This method is called after the invocation of each test method in the class. } func test_LabelExists() throws { for index in 0..<10 { XCTAssertTrue(frameBowlLabels[index][0].exists) XCTAssertTrue(frameBowlLabels[index][1].exists) XCTAssertTrue(frameScoreLabels[index].exists) } XCTAssertTrue(totalScoreLabel.exists) } func test_RecordOneFrame_TwoBowl_ShouldBeRecordedAndAtFirstFrame() throws { XCTAssertEqual(frameBowlLabels[0][0].label, "-") XCTAssertEqual(frameBowlLabels[0][1].label, "-") XCTAssertEqual(frameScoreLabels[0].label, "-") bowlButtons[1].tap() XCTAssertEqual(frameBowlLabels[0][0].label, "1") XCTAssertEqual(frameBowlLabels[0][1].label, "-") XCTAssertEqual(frameScoreLabels[0].label, "-") bowlButtons[5].tap() XCTAssertEqual(frameBowlLabels[0][0].label, "1") XCTAssertEqual(frameBowlLabels[0][1].label, "5") XCTAssertEqual(frameScoreLabels[0].label, "6") } } |
テストの追加
Refactoring して、コードが見易くなり、追加もしやすくなりましたので、10フレームまでの新しいテストを追加して、確認することにします。
テスト用のユーティリティ関数も追加しました。前回のテストは、1行になっています。
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 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 |
// // TDDBowlingUITests.swift // // Created by : Tomoaki Yagishita on 2020/11/08 // © 2020 SmallDeskSoftware // import XCTest class TDDBowlingUITests: XCTestCase { var bowlButtons: [XCUIElement] = [] var frameBowlLabels:[[XCUIElement]] = [[XCUIElement]]() var frameScoreLabels: [XCUIElement] = [] var totalScoreLabel: XCUIElement! override func setUpWithError() throws { // In UI tests it is usually best to stop immediately when a failure occurs. continueAfterFailure = true let app = XCUIApplication() app.launch() for index in 0..<10 { let button = app.buttons["Button\(index)"] bowlButtons.append(button) let bowl0 = app.staticTexts["FrameBowlView\(index)-0"] let bowl1 = app.staticTexts["FrameBowlView\(index)-1"] frameBowlLabels.append([bowl0, bowl1]) let score = app.staticTexts["FrameScoreView\(index)"] frameScoreLabels.append(score) } totalScoreLabel = app.staticTexts["TotalScoreView"] } override func tearDownWithError() throws { // Put teardown code here. This method is called after the invocation of each test method in the class. } func test_LabelExists() throws { for index in 0..<10 { XCTAssertTrue(frameBowlLabels[index][0].exists) XCTAssertTrue(frameBowlLabels[index][1].exists) XCTAssertTrue(frameScoreLabels[index].exists) } XCTAssertTrue(totalScoreLabel.exists) } func test_RecordOneFrame_TwoBowl_ShouldBeRecordedAndAtFirstFrame() throws { checkAndThrowFrame(prevResult: 0, frameIndex: 0, num1: 1, num2: 5) // XCTAssertEqual(frameBowlLabels[0][0].label, "-") // XCTAssertEqual(frameBowlLabels[0][1].label, "-") // XCTAssertEqual(frameScoreLabels[0].label, "-") // // bowlButtons[1].tap() // XCTAssertEqual(frameBowlLabels[0][0].label, "1") // XCTAssertEqual(frameBowlLabels[0][1].label, "-") // XCTAssertEqual(frameScoreLabels[0].label, "-") // // bowlButtons[5].tap() // XCTAssertEqual(frameBowlLabels[0][0].label, "1") // XCTAssertEqual(frameBowlLabels[0][1].label, "5") // XCTAssertEqual(frameScoreLabels[0].label, "6") } func test_ScoreAtEachFrame_From0To9FrameNoSpareNoStrike_CorrectlyDisplayed() { var localScore:Int = 0 for index in 0..<10 { let (bowl0, bowl1) = randomBowlForFrame() checkAndThrowFrame(prevResult: localScore, frameIndex: index, num1: bowl0, num2: bowl1) localScore = localScore + bowl0 + bowl1 } XCTAssertEqual(totalScoreLabel.label, String(localScore)) } // utility func checkAndThrowFrame(prevResult: Int, frameIndex:Int, num1:Int, num2:Int) { XCTAssertEqual(frameBowlLabels[frameIndex][0].label, "-") XCTAssertEqual(frameBowlLabels[frameIndex][1].label, "-") XCTAssertEqual(frameScoreLabels[frameIndex].label, "-") self.bowlButtons[num1].tap() XCTAssertEqual(frameBowlLabels[frameIndex][0].label, String(num1)) XCTAssertEqual(frameBowlLabels[frameIndex][1].label, "-") XCTAssertEqual(frameScoreLabels[frameIndex].label, "-") self.bowlButtons[num2].tap() XCTAssertEqual(frameBowlLabels[frameIndex][0].label, String(num1)) XCTAssertEqual(frameBowlLabels[frameIndex][1].label, String(num2)) XCTAssertEqual(frameScoreLabels[frameIndex].label, String(prevResult + num1 + num2)) } func randomBowlForFrame() -> (Int, Int) { let bowl0 = Int.random(in: 0..<10) let bowl1 = Int.random(in: 0..<(10-bowl0)) return (bowl0, bowl1) } } |
シミュレータで動作させると、どんどんスコアが追加されていくのが見えて、面白いです。
まとめ その3
10フレームまで記録できるようにはなりました。
次に、スペア・ストライクの表示対応とスコア計算対応と進めていく予定です。
説明は以上です。次回に続きます。Happy Coding!
Sponsor Link