fastlaneの便利な機能を徹底的に活用しよう

[fastlane](https://fastlane.tools/)活用してますか？ `fastlane init`でセットアップした設定のままで満足してませんか？

iOS Advent Calendar 2017 4日目はそんなfastlaneのあまり使われていない便利な機能やTipsを、内部実装を参照しながらご紹介したいと思います。

:warning:  fastlaneに関する基本的な情報は省いています。

# ***file

fastlaneには`Fastfile`の他に、`Appfile`、`Deliverfile`、`Gymfile`、`Matchfile`、`Scanfile`、`Snapfile`といった `***file`という名前のファイルがあります。

これらのファイルはアクションのオプションの設定を外部ファイル化したもので、`$ fastlane アクション名 init`で生成できます。
例えば、Deliverfileでは`deliver`アクションのオプションを設定できます。

そして、これらのファイルで設定した値はそれぞれのアクションのオプションで **上書き** することができます。
つまり、これらのファイルで設定した値は **アクションのデフォルト値** を設定していることと同じになります。
できるだけDRYな設定にするためにそのプロジェクトのデフォルト値を設定しておくのが良いと思います。

さらに実は `***file` も **Rubyスクリプトとして実行** されています。 ([参考: configuration_file.rb](https://github.com/fastlane/fastlane/blob/2f760b73d47805e1190980fefc8dbf84e2405366/fastlane_core/lib/fastlane_core/configuration/configuration_file.rb#L43))
このため例えば、FastfileやGymfile、Scanfileで同じschemeを設定したい時などは更に外部ファイルにまとめることもできます。

```ruby:fastlane/global_config.rb
$scheme = 'Foo'
```

```ruby:fastlane/Gymfile
require './global_config'
scheme $scheme
```

# スクリーンショット

既存のiTunes Connectのスクリーンショットをfastlaneで管理するようにしたい場合は、下記のコマンドで設定を取り込むことができます。

```bash
$ fastlane deliver download_screenshots
```

このコマンドでダウンロードされるスクリーンショットの画像名は以下のように非常に扱いにくい名前になっています。

```
1_ipadPro_1.ftl_d2b40876dd4600114a62fe4b1d1ee2c5_7ae61c6210ffe64c7bfa51cb40c17958.png
1_iphone58_1.ftl_37f4b8484f232d8bfbde853b848faf81_525ba82cc2fb892163529a172224df4b.png
1_iphone6Plus_1.ftl_60c95154387f3d3fe611e8d9a4e2f7ee_2c59dd30d86e2cc938fe1c675fc1832b.png
1_watch_1.ftl_75517b31b5093f6a6879c4673f1a3c90_a4d298b28726c4a657250fadf800fd64.png
2_ipadPro_2.ftl_7cc6f697861a58f6dceb8fd5aa9f00f1_5be9ecf0343b597a23b2cfe7f9c795b0.png
2_iphone58_2.ftl_528d13d986139e781d5f7310a5f71a3a_48ffbe288dc369fd8c079464c2af0c59.png
2_iphone6Plus_2.ftl_a7b9bf20b28549ac0fb1f06f6b32fd88_17b7bf164c9decc3806795314310ef3b.png
2_watch_2.ftl_2f25837af0fd6f968c500217a9ca0c22_9f0e89930847cff409588e3cdb316829.png
3_ipadPro_3.ftl_e3cfaf3978e49c6f87492f6059451a38_161f6bd5437334fd2fc65fd2faf1f527.png
3_iphone58_3.ftl_7b2943a9db21fed000c8b5c0577c0434_3010e41650dcbb7d9aabb81b09d252ff.png
3_iphone6Plus_3.ftl_3c0893d58dc5ce1c2cf245e1662266be_4d4e4e5b949c786f5380bc6b372cf5ee.png
3_watch_3.ftl_2e484db3397f33a725fde60b896e6a62_d1fdb056a78b2961ca2c310f0ca99109.png
4_ipadPro_4.ftl_a4abdc4ddfdf7998e96b2a5eff0b8c90_e49cd62241ee6f8e88bad1b5dcc6e6c9.png
4_iphone58_4.ftl_d66b7ee5b109bef79504c3e198386f59_1de1201d6232244de72c04817ca2bcf5.png
4_iphone6Plus_4.ftl_c4a75fb5e58ea431c1f5f02d77e2aa4f_ab533e3c5ce237d3d141d10ca1104912.png
4_watch_4.ftl_3381b4960ca680c4d3f144d51d1f7438_fe50746a1adbaba0572fa2c0f0f84c71.png
5_ipadPro_5.ftl_447fe5604548d2bb7f3a5d81f312bf4d_e606d0e1bb6fa0c2bf706dcb10c5d0a4.png
5_iphone58_5.ftl_b6f225a3eb25f4a6a79875ad46747e4d_546df9dcb5b9c62e676cb50d21d674a1.png
5_iphone6Plus_5.ftl_31e005781ebf1a8e81fcbeba0789164d_7ad4de588942a91ec6fb391084e5ac9f.png
```

この名前、実は自由に変えることができます。
内部では画像サイズからデバイスの種類が判定されています。 ([参考: app_screenshot.rb](https://github.com/fastlane/fastlane/blob/0faca0a15a8719c70f991e144e277c9e4add19b2/deliver/lib/deliver/app_screenshot.rb#L107))

そして、肝心のスクリーンショットの配置順ですが、これはファイル名の昇順で配置されます。 ([参考: upload_screenshots.rb](https://github.com/fastlane/fastlane/blob/38e4e92aa5fa49d0260b71a5f117a8f1982ef125/deliver/lib/deliver/upload_screenshots.rb#L95))

つまり、ソート順を意識した名前さえ付けておけばよいわけです。例えば、このような感じです。

```
1_12.9inch.png
1_5.5inch.png
1_5.8inch.png
1_watch.png
2_12.9inch.png
2_5.5inch.png
2_5.8inch.png
2_watch.png
3_12.9inch.png
3_5.5inch.png
3_5.8inch.png
3_watch.png
4_12.9inch.png
4_5.5inch.png
4_5.8inch.png
4_watch.png
5_12.9inch.png
5_5.5inch.png
5_5.8inch.png
```

とてもわかりやすくなりましたね :+1:

ついでですが、その他メタデータはこのコマンドで既存のiTCの設定を取り込めます。

```bash
$ fastlane deliver download_metadata
```

# アクションを作る

fastlaneには便利なアクションが数多く用意されています。アクションの一覧は [ここ](https://docs.fastlane.tools/actions/) で確認できます。

ほとんどの場合は既存のアクションを組み合わせることで機能を実現できますが、欲しいアクションがなく、痒いところに手が届かな〜〜いということもあると思います。 :disappointed_relieved:
そのような時は、まず外部プラグインがないか [ここ](https://docs.fastlane.tools/plugins/available-plugins/) で探しましょう。そして、それでも見つからない時は自分で作ることになります。

## プロジェクト設定

fastlaneにはBundle IdentifierやTeam IDなどを変更できる機能があります。
このようにプロジェクト設定を変更できる機能は、CI/CDで複数の検証用アプリを作り分ける際に非常に便利です。

### ビルド番号の更新

自分でアクションを作る必要がある機能の一つに、ビルド番号の更新があります。

fastlaneには`increment_build_number`というビルド番号を1つ更新してくれるアクションがあります。
内部的には`agvtool next-version -all`というコマンドを実行しているのですが、残念ながらこれが上手く動作しない場合があります。

ビルド番号を「0」, 「1」, 「2」というような番号で管理している場合は問題ないですが、例えばストアバージョンを 「3.0.0」、 ビルド番号を 「3.0.0.5」 というような形式で管理している場合、`increment_build_number`実行後のビルド番号は「3.0.0.6」を期待しますが、実際には「4」になってしまいます :cry:

このような場合は、既存のアクションを組み合わせて独自のアクションを作ると良いです。

```ruby:Fastfile
lane :custom_increment_build_number do
  # get_build_numberを利用し、現在のビルド番号を取得、末尾の値を取得
  trailing_number = get_build_number.split('.').last.to_i
  # get_version_numberを利用し、ストアバージョンを取得、ビルド番号の末尾の値を1つ増やして連結
  next_build_number = "#{get_version_number}.#{trailing_number + 1}"
  # increment_build_numberのオプションに作成したビルド番号を指定し、更新
  increment_build_number(build_number: next_build_number)
end
```

このように既存のアクションを組み合わせて独自の処理を挟むだけで、簡単に既存のプロジェクトの管理方法に合わせた処理を作ることが出来ます。

### App Groupsを無効化する

fastlaneの依存ライブラリには[xcodeproj](https://github.com/CocoaPods/Xcodeproj)というgemがあり、fastlaneの内部でも多用されています。自分で作る際にはこのgemを活用することをおすすめします。すでにfastlaneで使用されているので、別途gemを入れる必要はありません。

xcodeprojを使った一例ですが、CapabilitiesのApp GroupsをOFFにする機能を作ってみます。

```ruby:Fastfile
lane :disable_app_groups do
  project = Xcodeproj::Project.open('../Foo.xcodeproj') # Fooというプロジェクトの場合
  targets_attributes = project.root_object.attributes['TargetAttributes']

  project.targets.each do |target|
    target_attributes = targets_attributes[target.uuid]
    capabilities = target_attributes['SystemCapabilities']
    capabilities&.delete 'com.apple.ApplicationGroups.iOS' # Ruby2.3以上

    target.build_configuration_list.build_configurations.each do |build_configuration|
      entitlements_path = build_configuration.build_settings['CODE_SIGN_ENTITLEMENTS']
      next unless entitlements_path
      entitlements_path = "./../#{entitlements_path}"
      entitlements = Xcodeproj::Plist.read_from_path(entitlements_path)
      if entitlements
        entitlements.delete 'com.apple.security.application-groups'
        Xcodeproj::Plist.write_to_path(entitlements, entitlements_path)
      end
    end
  end

  project.save
end
```

難しそうなこともxcodeprojを利用すると意外とできたりします。足りない機能はこのようにして補っていきましょう。
また、Fastfileにべた書きしていくとFastfileが肥大化してしまうので、プラグイン化や外部ファイル化していくことをおすすめします。
プラグイン化の方法は [公式のドキュメント](https://docs.fastlane.tools/plugins/create-plugin/) が参考になります。

便利なアクションが出来たら、本家にプルリクエストを送ってみるのも良いかもしれません :smile:

# 内部の便利クラスを使う

fastlaneには用意されたアクション以外にもアクションの機能実装に利用されている便利なクラスがあります。
これらのクラスを活用すると、アクションの組み合わせでは実現できない機能も作ることが出来ます。

今回はビルドの処理中を待ち、完了後、申請を出す機能を作ってみます。

## `Spaceship::Tunes`

このモジュールを利用すると iTunes Connect へログインしたり、アプリの情報を取得したり出来ます。

### ログインしていなければ、ログインする

```ruby
def login_itc_if_not_logged_in
  Spaceship::Tunes.login unless Spaceship::Tunes.client
end
```

AppFileで`apple_id`を設定し、一度ログインをしていれば、入力無しでログインできます。 (Keychainに保存されるため)

### iTunes Connectで設定したアプリの情報を取得する

```ruby
def find_app bundle_id
  login_itc_if_not_logged_in
  Spaceship::Tunes::Application.find bundle_id
end

app = find_app 'com.bar.foo.***'
puts app
#=> <Spaceship::Tunes::Application
#	apple_id="****",
#	name="<アプリ名>",
#	vendor_id="****",
#	bundle_id="****",
#	last_modified=<unixtime>,
#	issues_count=0,
#	app_icon_preview_url="https://****.png",
#	version_sets=[<Spaceship::Tunes::VersionSet
#	type="APP",
#	application=<Spaceship::Tunes::Application #<Object ...>>,
#	platform="ios">]>
```

アプリに関する様々な情報がとれます。 ([参考: application.rb](https://github.com/fastlane/fastlane/blob/33bf0721b283c692eaf7e8e312a6c7dade5cb6a4/spaceship/lib/spaceship/tunes/application.rb))

### 申請可能なビルドの中で、最新のビルドを取得する

```ruby
def latest_build app
  app.latest_version.candidate_builds
    .sort_by {|b| b.upload_date }
    .last
end

app = find_app 'com.bar.foo.***'
build = latest_build app
puts build.build_version
```

ビルドに関する様々な情報がとれます。 ([参考: build.rb](https://github.com/fastlane/fastlane/blob/39c4d1b8187540e3b633a31b4a1dca97ed354427/spaceship/lib/spaceship/tunes/build.rb))

### 処理中のビルドが終わるのを待つ

上記の機能を組み合わせてレーンを作ってみます。

```ruby:Fastfile
desc "Wait for the latest build processing"
lane :wait_for_process do |options|
  app = options[:app] || find_app 'com.bar.foo.***'
  loop do
    build = latest_build app
    break unless build
    break unless build.processing

    sleep 30 # 30秒待つ
  end
end
```

### 最新のビルドを次に申請するアプリのビルドとして設定する

```ruby:Fastfile
desc "Set the latest build to the latest app version"
lane :set_latest_build do |options|
  app = options[:app] || find_app 'com.bar.foo.***'
  build = latest_build app
  editing_app = app.edit_version
  editing_app.select_build build
  editing_app.save!
end
```

### 処理中ビルドの処理完了後、アプリを申請する

```ruby:Fastfile
desc "Submit the latest build"
lane :submit_app do |options|
  app = options[:app] || find_app 'com.bar.foo.***'
  wait_for_process(app: app)
  set_latest_build(app: app)
  app.create_submission.complete!
end
```

`deliver`のオプションを駆使してもある程度、処理を分割できますが、このように内部のクラスを活用すると、任意の粒度で処理を実行したり、処理内容をカスタマイズしたり出来ます。

CI/CDにこだわりたい方はぜひお試しを :thumbsup: 

# アクション名が変わる！？

実は [fastlane 2.68.0](https://github.com/fastlane/fastlane/releases/tag/2.68.0) に大きな変更が入っています。

> [fastlane] Rename core actions and make aliases for backwards compatibility (#10939) via Mark Pirri

コアアクションのリネーム & 後方互換性のためのエイリアス作成、とあるようにfastlaneのコア機能となっているアクションの名前が変更になったようです。

| 古い名前 | 新しい名前 |
|:--:|:--:|
| cert | get_certificates |
| deliver | upload_to_app_store |
| frameit | frame_screenshots |
| gym | build_ios_app, build_app |
| match | sync_code_signing |
| pem | get_push_certificate |
| pilot | upload_to_testflight |
| precheck | check_app_store_metadata |
| produce | create_app_online |
| scan | run_tests |
| screengrab | capture_android_screenshots |
| sigh | get_provisioning_profile |
| snapshot | capture_ios_screenshots, capture_screenshots |
| supply | upload_to_play_store |

アクションの意味が名前からわかるようになりましたね。

ちなみにですが、`fastlane init`を実行してできる初期状態のFastfileのアクション名も変わっていました。

```ruby:Fastfile
# 一部省略してます
fastlane_version "2.68.0"

default_platform :ios

platform :ios do
  desc "Runs all the tests"
  lane :test do
    run_tests
  end

  desc "Submit a new Beta Build to Apple TestFlight"
  desc "This will also make sure the profile is up to date"
  lane :beta do
    # match(type: "appstore") # more information: https://codesigning.guide
    build_app # more options available
    upload_to_testflight
  end

  desc "Deploy a new version to the App Store"
  lane :release do
    # sync_code_signing(type: "appstore")
    capture_screenshots
    build_app # Build your app - more options available
    upload_to_app_store(force: true)
    # frame_screenshots
  end
end
```

古い名前もエイリアスが貼ってあるため利用できますが、使えなくなる前に移行したほうが良さそうです。
(まるっと実装が移動しました: [gym.rb](https://github.com/fastlane/fastlane/blob/bde021888ae62e41f821291e8e482d5355a720e3/fastlane/lib/fastlane/actions/gym.rb#L10))

また、「build_ios_app, build_app」のようにプラットフォーム名がついているものとついていないもので2つ名前があるアクションがあります。
こちらは、今後、現在のプラットフォームに合わせて挙動を **自動で切り替えてくれる** ようになるようです。
例えば、`build_app`と書いておくと、現在のプラットフォームがiOSであれば、`build_ios_app`、Androidであれば、`build_android_app`を裏で実行してくれるようになるようです。

同じ設定で複数のプラットフォームに対応できるようになる日も近いかもしれません。楽しみです。

# 最後に

時々ソースコードへのリンクを載せましたが、fastlaneで困ったときはソースを読むことをおすすめします。
非常に読みやすく書かれていますので、GitHubの検索を駆使して、メソッドなどを追っていくと意外とわかると思います。

そして、コードへの理解が深まった際にはぜひ、恩返しにコントリビューションしてみてください :raised_hand: 
凄まじいスピードでレビューし、マージしてくれますよ :+1:

以上で、iOS Advent Calendar 2017 4日目はおしまいです。
明日は @takasek さんです :tada: