「Start Developing iOS Apps (Swift)」でFoodTracker Appを実際に作ってみる その6[データモデルを定義する]

Start Developing iOS AppでFoodTrackerを実際に作ってみる、その6

公式にある「Start Developing iOS Apps (Swift)」を使って実際にFoodTracker Appを作ってみる、というシリーズの第六回目です。前回はこちら

「Start Developing iOS Apps (Swift)」でFoodTracker Appを実際に作ってみる その5[カスタムコントロールを実装する]
公式にある「Start Developing iOS Apps (Swift)」を使って実際にFoodTracker Appを作ってみる、と...

前回は料理を評価するための独自コントロールUIを作りました。今回はデータモデル(data model)を作ります。データモデルは、アプリに必要な情報を管理するためのもので、FoodTracker用にはクラスを1つ作ります。

この記事を書いている時点でのXcodeのバージョンは7.3です。

追記: Swift 3がリリースされたので、Swift 3/Xcode 8.0の仕様に合わせました。バージョンによる違い、警告やエラー等に関してはXcode 7.3の記述をそのまま載せる場合がありますが、その際は補足します。また、一部の画像は以前のバージョンのXcodeやシミュレータのモノを流用している場合があります。

データモデルを定義する | Define Your Data Model

いつもの学習目的「Learning Objectives」です。この回が終わったら、

  • データモデル(クラス、または独自型)を作れる
  • 自作クラスのfailable initializer(s)を作れる
  • Failable/nonfailable initializersの概念的な違いを説明できる
  • ユニットテストを作成・実行し、データモデルをテスト出来る

ようになることが目標です。今回メインのUIは触らないので、見た目的な変更はありません。

データモデルを作る | Create a Data Model

最初に述べたように、今回作るデータモデルは

データモデル = FoodTrackerアプリのsceneで表示したい情報を管理するモノ

です。データモデルとしてクラス(class)を作りますが、必ずしもデータモデル = クラスではないと思います。場合によっては構造体(structures)や列挙型(enumerations)を使う場合もあるかもしれません。

データモデルクラスを作る | To create a new data model class

では、ここからデータモデルクラスを作る手順を具体的に説明します。手順はPart5でRatingControlを作った時とほぼ同じです。

新規ファイルを開き、「iOS – Source」、「Swift file」を選択

新規ファイルを開くには、Xcodeのメニューから

File > New > File…(またはキーボードショートカット⌘N

です。キーボードショートカットの⌘Nは新規ファイルを開くためのコマンドですが、それ以外にも

  • ⇧⌘N: 新規プロジェクトを開く
  • ⇧⌥⌘N: 新規プレイグラウンドを開く
  • ⌃⌘N: 新規ワークスペースを開く

というキーボードショートカットがあるので、覚えておくと便利かもしれません。

Xcode: New fileでSwift Fileを選択
新規ファイル作成画面が出てくるので、上部メニュー項目の一番上「iOS」にある「Source」の「Swift File」を選択し、右下の「Next」をクリック。

RatingControlの場合との違いは、「Swift File」を選んだことです(RatingControlでは「Cocoa Touch Class」を選択)。これは、データモデルとして作るクラスが基底クラス(base class)で、既存のクラスを継承する必要がないからです。

名前を「Meal.swift」として保存

ファイル名を入力する画面になるので、保存するファイル名として

Meal.swift

を選択。拡張子(.swift)を表示しないオプションの場合は、.swiftが出ていないかもしれないので、その場合は「Meal」だけをタイプ。

Xcode: FoodTrackerTestsを保存先として選択
また、ファイルの保存先指定では「FoodTrackerTests」をターゲットとして忘れずに選択(上の画像参照)。デフォルトでは、「FoodTracker」にのみチェックが入っているはずです。

最後に右下の「Create」をクリックしてファイルを作成。「Meal.swift」という新規ファイルが作られます。

料理のデータモデルを定義する | To define a data model for a meal

料理のデータとして必要なのは、

  • 料理の名前
  • 料理の写真
  • 料理の評価

ですから、これらをプロパティ(properties)としてMeal.swiftに定義します。料理の名前は当然String、料理の評価は5段階(+評価なし)なのでIntで良さそうです。写真はUIImageを使いますが、料理の写真が無いケースを考慮してオプショナル型にします。

「import Foundation」を「import UIKit」に書き換える

Meal.swiftを開いた状態だと思うので、中身を見ると、最初のコメント以外の記述は

import Foundation

のみです。これを、

import UIKit

に書き換えます。

By default, a Swift file imports the Foundation framework so you can work with Foundation data structures in your code. You’ll be working with a class from the UIKit framework, so you need to include UIKit in your import statement. Importing UIKit also gets you access to Foundation, so you can remove the redundant import to Foundation.

と書いてあるように、UIKitのクラスなどを使っていくのでimportしておく必要があります。また、FoundationUIKitに含まれるので、UIKitをimportした場合はimport Foundationの記述は削除して構いません。

Mealクラスを作ってプロパティを定義

まず、Mealクラスと3つのプロパティ(名前、写真、評価)を定義します。

class Meal {
    // MARK: Properties
    
    var name: String
    var photo: UIImage?
    var rating: Int
}

いつもの// MARK: コメントを入れてから、必要なプロパティの名前と型を定義します。全て変数(var)で定義していて、定数(let)ではありません。これは後から変更することを想定した仕様です。

Xcode: Mealクラス。初期化子が無いというコンパイルエラー
この段階でXcodeはエラーを吐いているはずで、そのエラーをチェックすると、どうやら初期化子が無い(no initializers)といって怒られているようです(上の画像参照)。したがって、次は初期化子を作ります。

Mealクラスの初期化子を作る

初期化子(initializers)というのは、(クラスなどの)インスタンスを使用可能な状態にするための特別なメソッドで、その仕事は格納プロパティの初期化などです。先程定義したプロパティの下に

// MARK: Initialization
init(name: String, photo: UIImage?, rating: Int) {
}

という感じで初期化子を作ります。

初期化子の外枠を作ったら、プロパティをパラメータで初期化します。

// Initialize stored properties
self.name = name
self.photo = photo
self.rating = rating

これで基本的には完成ですが、もう少し工夫します。

エラーチェックを入れる

現時点での初期化子は入力したパラメータをそのままプロパティに代入して終了しています。もしユーザが料理名に空白文字を入力したり、評価値として負の値を入力した場合、アプリが想定外の振る舞いをする可能性があります。最悪の場合アプリがクラッシュします。

このようなケースを予め避けるために、初期化子最後に条件判定を入れて回避します。

// Initialization should fail if there is no name or if the rating is negative
if name.isEmpty || rating < 0 {
    return nil
}

今避けたいのは「料理名が空白(name.isEmpty)」という条件と、「評価値が負(rating < 0)」という条件ですから、それらをOR(||)で繋ぎます。ORの場合はどちらか1つでも真(true)であれば条件を満たします。また、これらの条件を満たした場合は、インスタンスそのものを作らないという選択をしたいので、return nilとしています。

Fix-it機能でfailable initializerに変換する

この時点ではエラーが出ているはずで、

Only a failable initializer can return 'nil'

といって怒られていることが分かります。初期化子でnilを返すことが可能なのはfailable initializerだけで、init?のようにinitの後ろにクエスチョンマーク?を付けないといけません

Xcode: コンパイルエラー。Fix-it機能でfailable initializerに変換
手で?を入れてもよいですが、上記画像のようにFix-it機能を使ってfailable initializerの自動補完が可能なので利用します。

Part5でも使ったFix-it機能ですが、Xcodeがコンパイルエラーの修正方法を自動で検知して教えてくれる機能です。コンパイルエラーが出ている場合、行番号左側に出ている白抜きの赤い丸をクリックすると、エラーメッセージと同時に「こうすれば修正できるけど修正するか?」というエラー修正メッセージ(Fix-it)も出て来ます。Fix-itと表示されている部分をダブルクリックする(または選択された状態でReturnキーを押す)と、Xcodeが自動的に修正をかけてコンパイルエラーを取り除いてくれます。

初期化子をビルド

出来た初期化子は

// MARK: Initialization

init?(name: String, photo: UIImage?, rating: Int) {
    // Initialize stored properties
    self.name = name
    self.photo = photo
    self.rating = rating

    // Initialization should fail if there is no name or if the rating is negative
    if name.isEmpty || rating < 0 {
        return nil
    }
}

このようになっているはずです。

念のためこの時点で問題が無いかどうか、Meal.swiftをビルドします。Xcodeのメニューから、

Product > Build(またはキーボードショートカット⌘B)

を選択してビルドします。何も問題が無い場合は、

Build succeeded

と表示されるはずです。

nonfailable or failable initializers

通常の初期化子(nonfailable initializers)とfailable initializersの違いは、

出来るインスタンスがnilになるかどうか

です。別の言い方をすると、failable initializerで作るインスタンスはオプショナル型である、と言えます。通常の初期化子の場合は、先程コンパイルエラーになったことからも分かるように、インスタンスをnilにすることは出来ません。

データをテストする | Test Your Data

作ったデータモデルをビルドして問題が無いことを確認しましたが、実際にアプリで動かした時に本当に問題が無いのかは分かりません。実装した機能が予想通り動かないかもしれないし、予期しない挙動で実行時エラーを起こす可能性が無きにしもあらずです。その不安(問題)を解決してくれるのがユニットテスト(unit tests)と呼ばれている機能です。

ユニットテストを使うと、アプリ全体ではなくて、アプリの特定部分限定で挙動をテストすることが出来ます。したがって、例えば今作ったMealクラスだけに絞って、その挙動の確認をすることが可能になります。

FoodTrackerのユニットテスト用ファイルを確認する | To look at the unit test file for FoodTracker

Unit test用ディレクトリ

Navigator areaでFoodTrackerの階層構造を見ると、

Xcode: Unit testファイルをnavigator areaで確認
FoodTrackerTestsというディレクトリがあるのが分かります。これがユニットテスト用のファイルが収められている場所になります。

FoodTrackerTests概観

ここにあるFoodTrackerTests.swiftを開いて眺めてみます。

import XCTest
@testable import FoodTracker

class FoodTrackerTests: XCTestCase {
    
    override func setUp() {
        super.setUp()
        // Put setup code here. This method is called before the invocation of each test method in the class.
    }
    
    override func tearDown() {
        // Put teardown code here. This method is called after the invocation of each test method in the class.
        super.tearDown()
    }
    
    func testExample() {
        // This is an example of a functional test case.
        // Use XCTAssert and related functions to verify your tests produce the correct results.
    }
    
    func testPerformanceExample() {
        // This is an example of a performance test case.
        self.measure {
            // Put the code you want to measure the time of here.
        }
    }
    
}

最初にXCTestをインポートしていますが、このXCTestがXcodeのプロジェクトでunit testを作ったり実行するのに必要な機構です。次の行にある、

@testable import FoodTracker

@testable(testable attribute)は、FoodTrackerをunit testに使えるようにするための宣言です。アクセスコントロールの一種のようで、public宣言するのと同じ効果があるみたいです。

FoodTrackerTestsというクラスが本体で、XCTestCaseというクラスを継承しています。デフォルトでは(空の)メソッドが4つ定義されていて、最初の2つsetUp()tearDown()オーバーライドされています。詳しい説明(英語)は、公式のクラスリファレンス

XCTestCase
https://developer.apple.com/reference/xctest/xctestcase

に載っていますが、今回のテストではオーバーライドしたメソッドは使用しないようです。

機能or処理速度のチェック

基本的に、ユニットテストで確認する項目は2つで

  • アプリの機能: 実装したプロパティ等々が、ちゃんと期待した値を返すかどうか
  • アプリの処理速度: コードが期待した処理速度を出しているか

です。今回のテストでは処理速度を確認するほど大層なものは実装していないので、機能面の簡単なテストを試しに実行してみます。

また、メソッドを追加する場合には、必ず頭にtestを付けて、その後に続く名前は「何をテストしているのか?」ということが分かるように命名するべし、と書かれてますので、なるべくそうしましょう。この後初期化子をテストするメソッドを実装しますが、その場合ならtestMealInitialization()とすれば分かりやすいです。

Mealオブジェクト初期化のユニットテスト(メソッド)を作る | To write a unit test for Meal object initialization

今回はMealクラスの初期化子をテストするためのメソッド、testMealInitialization()を実装します。以下の説明で使用する言葉の定義ですが、

  • ユニットテスト(unit test) = テスト用に実装されるメソッド
  • テストケース(test case) = ユニットテスト内部に記述する実際のテスト

となっています。ユニットテストと呼んでいるのは、実装したメソッドのことを指しています。また、実際にテストするために書かれたメソッド内部のコードを「テストケース(test case)」と呼んでいます。

用意されているメソッドを削除してtestMealInitialization()を作成

テンプレートで用意されている4つのメソッドを全て削除して、FoodTrackerTestsの中身を空にします。

import XCTest
@testable import FoodTracker

class FoodTrackerTests: XCTestCase {
    
}

ここにいつもの// MARK: コメントを追加します。

// MARK: FoodTracker Tests

このコメントを入れるのは、(1) コード上で何が書いてあるのかを(人間が分かる言葉で)可視化する、(2) function menuから検索可能にする、という理由からです。日本語でコメントを書いた方が分かりやすいかもしれません。

次に、メソッドの外枠testMealInitialization()を先程のコメント直下に追加します。

// Tests to confirm that the Meal initializer returns
// when no name or a nagative rating is provided
func testMealInitialization() {
}

メソッドの上にあるコメントは、これから実装するテスト内容を書いたものです。

初期化子のテスト(test case)を追加する | 初期化が成功するケース

まず、初期化がうまくいった場合をテストしてみます。

// Success case
let potentialItem = Meal(name: "Newest meal", photo: nil, rating: 5)
XCTAssertNotNil(potentialItem)

パラメータに名前と評価値を入れてMealクラスを初期化し、それをXCTAssertNotNil(_:)という関数に放り込んでいます。XCTAssertNotNil(_:)は、放り込んだパラメータがnilじゃなければ成功、nilなら失敗(終了)、という関数です。

XCTAssertNotNil(_:)は、最初はXCTestCaseのメソッドかと思いましたが、グローバルに定義されている関数のようです。

今、potentialItemはインスタンスとして正しく生成されているはずなので、XCTAssertNotNil(_:)によるテストは「成功」するはずです。

初期化子のテスト(test case)を追加する | 初期化が失敗するケース

同じようにテストを追加しますが、次は初期化が失敗するケースを2つ追加します。Mealクラスの初期化子に

// Initialization should fail if there is no name or if the rating is negative
if name.isEmpty || rating < 0 {
    return nil
}

という感じで、「文字列が空白」または「評価値が負」ならnilになるという条件式を追加しました。これを実際にテストしてみます。

// Failure case
let noName = Meal(name: "", photo: nil, rating: 0)
XCTAssertNil(noName, "Empty name is invalid")

let badRating = Meal(name: "Really bad rating", photo: nil, rating: -1)
XCTAssertNotNil(badRating)

最初の定数noNameは名前が空白文字になっており、2つ目の定数badRatingは評価値が負になっています。どちらも初期化には失敗しているので、インスタンスは生成されずnoNamebadRatingnilになっているはずです。

noNameの方では、XCTAssertNil(_:)という関数を使っています。これはパラメータがnilなら成功で、nilじゃなければ失敗という関数で、丁度XCTAssertNotNil(_:)の逆バージョンです(関数名からも自明だと思います)。今、noNamenilになっているので、XCTAssertNil(_:)関数によるテストは「成功」するはずです。

次に、badRatingのテストは最初に使ったXCTAssertNotNil(_:)で行います。この関数はパラメータがnilの場合は失敗する関数なので、このテストは「失敗」するはずです。

テストを実行する前に、今回のテスト用に実装したコード全体を見てみると

import XCTest
@testable import FoodTracker

class FoodTrackerTests: XCTestCase {
    // MARK: FoodTracker Tests

    // Tests to confirm that the Meal initializer returns
    // when no name or a nagative rating is provided
    func testMealInitialization() {
        // Success case
        let potentialItem = Meal(name: "Newest meal", photo: nil, rating: 5)
        XCTAssertNotNil(potentialItem)
        
        // Failure case
        let noName = Meal(name: "", photo: nil, rating: 0)
        XCTAssertNil(noName, "Empty name is invalid")

        let badRating = Meal(name: "Really bad rating", photo: nil, rating: -1)
        XCTAssertNotNil(badRating)
    }
}

このようになっているはずです。これからこのテストを実行しますが、3つ目のテスト(XCTAssertNotNil(badRating))でわざと失敗するようにコードを組んだので、実際にそうなるか確認します。

ユニットテストtestMealInitialization()を実行してみる | To run the testMealInitialization() unit test

今作ったユニットテストを実行するためには、

Product > Test(またはキーボードショートカット⌘U)

を選択します。これだと作ったユニットテスト全体の実行になるので、ユニットテスト単体で実行したい場合は、

Xcode: ユニットテストをXcodeのeditor areaから実行
上の画像のように、ユニットテスト左側に表示されている菱型マークのようなボタンにマウスを持ってくると、実行ボタンが表示されますので、それをクリックします。

実際に実行した後のXcodeでの表示が、

Xcode: ユニットテスト実行後。最後のテストで失敗
このようになるはずです。3つ目のテストで、ちゃんと失敗しているのが分かります。今はわざと失敗するように書きましたが、実際のコードでこのようなエラーが予期せずに起こった場合は、コードに何かしらの問題があることが分かります。

テストケースを直す | To fix the test case

今、3つ目のテストケースは

XCTAssertNotNil(badRating)

こうなっています。badRatingnilなので、「成功」させるにはXCTAssertNil(_:)を使います。

ですから、上記コードを

XCTAssertNil(badRating, "Negative ratings are invalid, be positive")

で書き換えて、再度ビルドしてテストします。

テストがうまくいくと、
Xcode: 全てのテストケースが成功した場合の表示
最終的にはこのような表示になります。

Unit testing is an essential part of writing code because it helps you catch errors that you might otherwise overlook. As implied by their name, it’s important to keep unit tests modular. Each test should check for a specific, basic type of behavior. If you write unit tests that are long or complicated, it’ll be harder to track down exactly what’s going wrong.

とありますが、ユニットテストのコードを書く時のポイントは、

1つ1つのユニットテストを簡潔にまとめること

です。ユニットテストの目的はソースコードの間違い探し(デバッグ)なので、長くて複雑なテストを作るよりも、簡潔なテストの方が有効なはずです。

まとめ

今回はほぼ初期化子とユニットテストの話でした。ユニットテストは実際に使いながら覚えていくのが良いかと思います。

次回は、table viewを使って料理の写真と評価値をリスト表示するUIを作ります。

スポンサーリンク
広告1
広告1

シェアする

フォローする