[TDD][SwiftUI] SwiftUI と TDD で作る ボーリングスコアアプリ(その1 モデル作成 Part1)

ボーリングアプリ(スコアを記録するアプリ)を、SwiftUI と TDD で作ってみます。

環境&前提

環境&前提
  • macOS Catalina 10.15.7
  • Xcode 12.1
  • MVVM (Model-View-ViewModel) で作ります
  • TDD で作るので、テスト作ってから、実装します。(基本的には、YAGINI(You ain’t gonna need it)で、必要になってから実装します)
  • 最適化しない(メモリ効率や実行スピードは考慮しない、いわゆる富豪的プログラミングでいきます)

アプリ概要

以下の方針で作りますが、途中で必要に応じてアップデートしていきます。

アプリ概要
  • 1投ごとに記録できる
  • GUI は、最低限。例えば、スペアは、”/” で表示され、ストライクは、✖️ で表示される。アニメーションとかしない
  • フレームごとに、そこまでの合計点数が計算されて、表示される
  • 1人分の1ゲーム分を記録表示する

アプリ設計

TDD で作るとは言っても、簡単な設計は行います。(1項目で5分以上悩んだら、設計過多ということで、設計段階での作り込みをしないようにシンプルに作りました)

アプリ設計
  • MVVM の M (Model)
    • struct Frame は、1フレーム分を記録する
    • Frame が 10 集まって、 struct BowlingGame になる
    • Frame には、2投分のデータが保存されるが、10Frame 目は、3投するかも
    • 1投には、{未投,既投,不要}の種類があり、既投 であれば、倒した本数が記録されている。1〜9Frameの3投目は常に 不要 のハズ
    • 1Frame には、{スコア計算不可(まだ投げてない or 必要な情報がそろってない) と スコア計算可能} がある
    • 1Frame の 1投目が 10 であれば、2投目は記録できない
    • スコア計算は、都度計算され、保存されない
    • スコア計算できない時は、スコアとしては、-1 が返される
  • MVVM の VM (ViewModel)
    • Frame に記録されている1投目が 10 であれば、ストライク表示を行う
    • Frame に記録されている2投の合計が10であれば、スペア表示を行う
    • スコアが、-1 (計算不可)の時には、”-” がスコアとして表示される
  • MVVM の View (View)
    • Frame 表示には、FrameView を作る
    • FrameView は、BowlView x 2 + ScoreView で構成される
    • GameView は、FrameView を10と、TotalScoreViewを2つ持つ
    • GameView は、ViewModel を持つ

プロジェクトを作る

Xcode を使って、プロジェクトを作ります。

  • Template: “iOS” – “App” を選択
  • Project 設定: 以下を選択・設定
    • Product Name: TDDBowling
    • Interface: SwiftUI
    • Life Cycle: SwiftUI App
    • Language: Swift
    • Include Tests にチェックマーク
  • 適当な箇所に保存 (Create Git repository on my Mac にチェックしておくと git でバージョン管理できて幸せです)

Hello World を表示してくれるテンプレートが、動くことを確認します。

Model を作る

モデルを作ろうとするまえに、まずは、テストを作りましょう。

Includes Tests をチェックしたことで、テンプレートとして、2種類のテストが用意されています。”TDDBowlingTests” と “TDDBowlingUITests” です。

前者は、いわゆる UnitTest、後者は、UITest です。

モデルは、UITest ではテストできないので、UniteTest (TDDBowlingTests) にテストを追加します。

Model 用テストクラスの作成

モデルの名称は、BowlingGame にする予定なので、
新しく “BowlingGameTests” というファイル名のSwift ファイルを TDDBowlingTests に追加します。
Model は、”BowlingGame” という struct です。引数なしで、作成できるハズなので、以下のようなテストコードになります。

引数なしで struct が生成できるかどうかを確認するテストになってます。

BowlingGameTests code

当然テストを描いただけなので、コンパイルエラーになってしまい、実行できません。エラー:”Cannot find ‘BowlingGame’ in scope”

Model の作成

今度は、アプリケーション本体のコードを書いていきます。BowlingGame.swift を追加します。

BowlingGame.swift code
コード解説
  1. BowlingGame は、Frame を 10 保持します
  2. Frame は、Bowl を3つ保持します。3つの初期値は、それぞれ.NotYet, .NotYet, .NoNeed を指定しています
  3. Bowl は、enum で定義しています。NotYet は、未投球 を Done は、投球済を NoNeed は、10フレーム目の3投目のための設定です。Done の時には、倒れた本数を記録するため、assosiated type に Int を指定しました

# 1投は、”Throw” は、予約語とかぶるので、Bowl にしてみました。

懸念点は、以下の点ですが、テストで明らかになるまでは放置で。

  • 初期化する時に、10フレームの3投目まで NoNeed になってしまう

BowlingGame テストの追加 1投目を記録できるかテスト

BowlingGame に含まれる Frame をテストすることを考えると、当然、BowlingGame の保持する Frame に対するアクセサを考えることになります。

Frame に値をセットして、きちんと記録されていることをテストすることにしましょう。

シンプルに、bowlingGame.addBowlResult(Int) で、投球した結果を追加していくこととしました。どこに追加するかは、BowlingGame 側が考えるということで。

取得する時は、何フレーム/何投目を意識して取得するはずなので、bowlResult(frameIndex, inFrameIndex) -> Bowlとしました。

MEMO
当初、bowlingGame.setBowl(frameIndex, inFrameIndex, value) でセットできることを考えたのですが、いま何フレーム目の何投目かを誰かが記録しなければいけないので、やめました。

とりあえず(?)、ストライクやスペアでない 1投分 を追加して、記録できているかを確認できることをテストします。

BowlingGameTests code

当然、addBowl や bowlResult なんて メソッドは作ってないのでエラーです。

BowlingGame の追加実装

どのフレームに記録するかの判断は、それなりにロジックを書かないといけないので、後回しにして、
とりあえず、最初のフレームの最初の投球に記録することにしました。これでテストは通るはずです。

BowlingGame 追加 code

先ほど書いたテストは通ることが確認できます。

最初のフレームを記録できるかテスト(1投目を記録できるかテストの改良)

2投目も保存して、1フレーム分保存できるテストに変更しましょう

テスト内容に合わせて、テストのメソッド名も変更しました。

test_putScore_atFrame0Bowl0And1_shouldBeRecorded code

特に、2投目を想定して実装していないので、
2投目のテストで、”XCTAssertEqual failed: (“nil”) is not equal to (“Optional(5)”)”というエラーになります。

BowlingGame.addBowlResult の改良

手抜きして、常に 0-Frame の 0 投目に記録していましたが、きちんと記録するようにしましょう。

ここにきて、どこに記録すべきかをきちんと考えましょう。

最初の Frame の Bowl から辿って行って、最初に見つかった Bowl.NotYet の箇所に記録します。ですが、10フレーム目の 3投目の処理を考えなければいけません。

が、3投目は、10フレーム目が、ストライク or スペア の時のみ可能なので、もう少し後にしましょう。

BowlingGame.addBowlResult code

記録できる Frame/Bowl を探すための findRecordableFrameBowl メソッドを追加しました。

スペア・ストライクなしのテストをもう少し追加

力技ですが、10フレームまで、スペア・ストライク共にないときのテストケースを追加しましょう。

BowlingGameTests#test_recordScore_wholeGameWithOutSpareStrike_shouldBeRecorded code

スペアやストライクについてのロジック追加も残っていますが、ViewModel や View を作っていくことで、ここまでに作ってきたモデルを表示できるようにします。

説明は以上です。次回に続きます。Happy Coding!

コメントを残す

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