多言語対応。fastlane の snapshot で iOS アプリのスクリーンショットを自動的に作成する。 swiftで。

iOS アプリのデリバリを自動的に行う、[fastlane](https://github.com/fastlane/) というフレームワークがあります。

AppStore への自動デプロイなど、超便利な機能がたくさんありますが、そのツール群の中に snapshot という、
iOS シミュレータを用いて、スナップショットを自動生成してくれるイカすツールでがあります。

その使い方を紹介します。

本記事の内容としては、公式リポジトリの README に書いてある内容そのままなので、そちらを見た方が確実ではあります。
https://github.com/fastlane/snapshot

動画撮りました: https://youtu.be/yBtb_jre1-I
(※ただの動作させてる動画です。説明とか無いです)

# 何ができるのか

高度なスクリーンショットの自動作成ができます。

## マルチ画面サイズ対応

iPhone5, iPhone6, iPad など、画面サイズの違う各種デバイスを自動的に順番に起動し、スクリーンショットを撮影してくれます。

## 多言語対応

特に便利だと思ったのがコレ。多言語対応しているアプリの場合、シミュレータの言語を切り替えてスクリーンショットを撮影するのは面倒で骨の折れる作業ですが、snapshot ツールはそれも自動化してくれます。

処理の終了後には HTML ファイルを作って画像を一覧してくれるため、AppStore 提出用途だけでなく、多言語(かつ多画面サイズ)での表示確認用ツールとしてもすばらしく使い勝手が良いです。お昼休みに食事している間にスナップショットをひたすら作成させて、昼休み終わりにブラウザで確認。なんて使い方もできます。

## UI自動操作テスト対応

スナップショットの撮影は、Xcode の UIテストフレームワークの中で行うため、UIテストの自動操作中のあるポイントでのスナップショットが柔軟に撮影できます。

# インストール方法

snapshot 単体でのインストールもできると思いますが、fastlane ごとインストールするのが楽でしょう。

```bash
$ sudo gem install fastlane
```

アプリのプロジェクトルートで

```bash
$ fastlane init
```

# 設定ファイルの編集

fastlane/Snapfile ができるのでエディタで開きます。

devices, languages のリストを、用途に応じて編集します。

```ruby:Snapfile
...
devices([
  "iPhone 6",
  "iPhone 5",
])

languages([
  "en-US",
  "ja-JP",
])
...
```

この場合、英語と日本語でスクリーンショットを、iPhone6 と iPhone5 シミュレータでそれぞれ撮影します。

# 出力ディレクトリの作成

デフォルトでは、fastlane/screenshots に保存しますが、自動的には作ってくれないため作っておきます。

```bash
$ mkdir fastlane/screenshots
```

# テストフレームワークのセットアップ

## UIテストの追加

プロジェクトの TARGETS に、UIテストを追加します。

Xcode の、TARGETS の + → iOS Testing Bundle → 適当に名前をつけて Finish.

## ヘルパーファイルの追加

fastlane インストール時、 `fastlane/SnapshotHelper.swift` というファイルができているので、このファイルをマウスで Xcode の UIテストグループにドラッグ & ドロップします。
Add to targets: を聞かれるので、追加した UIテストにチェックが入っていることを確認し、Finish.

Objective-C の場合は、ヘッダファイルの用意など一手間必要なので、公式ページ参照。
https://github.com/fastlane/snapshot

## UIテストコードの編集

UIテストコードを開き、setUp メソッドを編集します。

デフォルトで、`XCUIApplication().launch()` が入っていますが、これはコメントアウトし

```swift
        let app = XCUIApplication()
        setupSnapshot(app)
        app.launch()
```

こう変えます。

そして、testXXX などのテストメソッド中に、

```swift
snapshot("ファイル名")
```

を実行すれば、スナップショットが作られます。

コメントを抜いたUIテストコードは以下のようになります。

```swift:HogeUITests.swift

import XCTest

class YTempoIOSUiTests: XCTestCase {

    override func setUp() {
        super.setUp()
        continueAfterFailure = false
        let app = XCUIApplication()
        setupSnapshot(app)
        app.launch()
    }

    override func tearDown() {
        super.tearDown()
    }

    func testExample() {
        snapshot("01-main-screen")
    }
}
```
この場合は、起動直後のスクリーンショットだけを保存します。

自動操作を行うには、このファイルの場合 testExample メソッドにカーソルを置き、左下の録画ボタンをクリック。

シミュレータが起動するので、操作すると操作内容がコードとして記録されます。

スクリーンショットを記録したいタイミングで `snapshot` 関数を呼ぶことで、自動的にスナップショットが作られます。いいね!

# 実行

デフォルトでは、fastlane の各レーンに組み込まれているため、

```bash
$ fastlane test
```

でも起動できますし、snapshot コマンド単体で起動することもできます。

```bash
$ snapshot
```

### 作成されたHTML

![スクリーンショット 2015-12-26 18.40.59.png](https://qiita-image-store.s3.amazonaws.com/0/68302/0ae8944b-2ecc-1f21-ccfc-6bb9d0fe913c.png)


# 課題

多言語化している場合、UIテストで

```swift:UITest.swift
let app = XCUIApplication()
app.buttons["English message"].tap()
```

このようなコードになっていると、English message が日本語に翻訳されている場合ボタンが見つからずに押せなくなります。

`XCUIElement` の `.exists` プロパティで、エレメントの存在テストができるため

```swift:UITest.swift
        let buttonLabels = [
            ["+ 1 BPM every 20 beats from 70 BPM", "Set"],
            ["20拍ごとに +1BPM、70BPM開始", "セット"],
        ]
        for labels in buttonLabels {
            if (app.buttons[labels[0]].exists){
                app.buttons[labels[0]].tap()
                snapshot("02-trainer-settings")
                app.buttons[labels[1]].tap()
                break
            }
        }
```
一応こんな感じで分岐はできますが…なんか他に良い方法ないですかね。

テストコード内で、ボタン名の判定を下記のようにNSLocalizedString にしても、OSの言語設定を評価してくれるわけではないので、ボタンを見つけられず失敗しました。

```swift:失敗コード
app.buttons[NSLocalizedString("Set", comment: "")].tap()

```