この記事はNikkei Advent Calendar 2021の3日目の記事です。
iOSチームの髙木です。先月紙面ビューアーに書き込みメモ機能を実装しました。みなさんぜひ利用してください!
本記事では、日経電子版・紙面ビューアー・Nikkei Waveといった日経電子版の主要なアプリ群に導入していった開発環境の改善及び、それらを利用したBitrise CIの効率的なキャッシング・並行処理を紹介します。
開発環境改善
Mint
Mintというパッケージマネージャを導入しました。 MintはSwift Package Manager で動くSwift製のCLIツールを管理することが可能です。 具体的には以下のツールを実際に管理しています。
- Carthage/Carthage
- mono0926/LicensePlist
- mac-cain13/R.swift
- realm/SwiftLint
- apple/swift-format
- yonaskolb/XcodeGen
Mintで管理すると以下のような利点があります。
- バージョン単位で管理できる
- バイナリが存在しない場合には、
swift build
してバイナリを用意し、それ以降はキャッシュして再利用する - プロジェクトをまたいで同一パッケージのキャッシュも共有できる
- 依存先はXcodeのみ、開発環境に振り回されるリスクを減らせる
特に最後の項は電子版サービスは開発だけでなくQA・テストなど様々なチームが利用するため、安定稼働しつつ不要な再インストールを避けることができる点で非常に良かったです。
XcodeGen
iOSアプリ開発をする上で避けては通れないproject.pbxproj
ファイルの管理の手間を減らすために、定義ファイル(YAML)からproject.pbxproj
を自動生成するXcodeGenを3つのアプリ全てに導入しました。
これは、project.pbxproj
ファイルを管理する上で避けて通れない以下の手間を削減します。
- 数万行の構造データ形式のファイル(
project.pbxproj
)から、数百行の定義ファイル(YAML)を管理するだけで良くなる - 変更波及箇所が明瞭で、Conflictが起きない。起きても容易に解決できる
- 複数のTargetに渡って行う共通処理をTemplate化することで、重複コードを無くせる
- 意図的に設定している
BuildSettings
の可視化・コメントも可能
特に日経電子版は、様々な施策を元に複数のバージョンを考慮した並行開発が盛んに行われるため、頻繁にproject.pbxproj
のConflict、そして解消方法がわからないDiff.......に悩まされていました。
現在は頻繁に複数バージョンの並行開発を行うことが進めやすくなりました。
また、紙面ビューアー・WaveにおいてはライブラリをCarthageからSPMに完全移行することができました。 その結果、現在は
RepositoryをClone → XcodeGen → Build
で開発が可能な環境となっています。ラクチン!
多くの改善ができる上で、1つ問題点を上げるとすればXcodeの最新の機能に追従できていないケースもあることです。
例えば、現状ではXcode 11
から導入されたTestPlanには未対応のままとなってしまっています。
Tips
3つのアプリ全てにXcodeGen移行を通して、自分なりのTipsをまとめました。 (3回も移行を行う方が他にもいるかはわかりませんが...) 基本的にBuildを繰り返し行えるように小さい単位で移行することが重要です。
- スペックの高い端末を用意する、ホント大事
- 弊社はエンジニアに最新のMacBook Proのハイスペックモデルなどが貸与される環境だったため、このステップの時間を大幅に減らすことが実際できました。
- プロジェクトの規模が最小のものを選択して、大まかな構成・書き方を設計する。共通したBuildScript処理などをまとめる
- ProjectSpec.mdをひたすら読む。ここが一番苦労します。
- 規模が大きいプロジェクトでは以下の優先順位で取り組む
- SPMの移行はできるだけ後にする、起動時にライブラリのFetchが毎回発生して時間がかかります!
- Carthageを使っている場合など、Frameworkが
Static Framework
かDynamic Framework
かを整理しておく。Buildができても実行時クラッシュを引き起こしうるため検証・テストを怠らないようにする - Embedded Framework を移行して、Framework単位のBuildが通ることを確認する
Debug/Prod
などの環境ごとの設定をまとめる- App本体を移行して、AppのBuildが通ることを確認する
- App Extensionsを移行してBuildが通ることを確認する
- Test Targetを移行して、Testが無事に通ることを確認する
CI 環境改善
本編です。 iOSアプリチームは基本的にCIツールにBitriseを利用しています。 Bitriseは動作環境(Stack)を選択する上で、Xcodeの更新が非常に早く、Beta版が導入されたStackも通常数日以内に提供される点で非常に重宝しています。
他にも
- 特にアプリで利用する汎用処理(署名・アーカイブ・デプロイ・etc...)が公式Stepとして提供されており、非常に簡単に設定できる
- rbenv / fastlane などの開発支援ツールもプリインストールされている
- CI設定を行うための優秀なGUI Editorが付属。各Stepのタイミングで利用できる変数がインタラクティブに可視化されることで、環境変数の受け渡しに関する手戻りが少ない。
等様々な良い点があります。 容量が大きいXcodeを事前にプリインストールされているCIサービスの中で更新の早さが唯一無二だと個人的に思っています。
しかし、それらを利用してもiOSアプリのビルドは一般的に非常に時間がかかります。 この項では、そんな長い時間がかかるCIを快適にするためにBitrise上で導入しているいくつかのTipsを紹介します。
キャッシング
定常的に行っていてキャッシュを利用しても弊害が少ない箇所のキャッシュ方法を実際に動作しているスクリプト例と共に紹介します。 以下の項目を対象としています。
- Homebrew
- Mint / XcodeGen
- Ruby Gem
- Swift Package Manager
Homebrew
Bitriseには公式にHomebrewのStepがあります。 しかしこれは利用しません。 公式のステップでは、あくまでキャッシュを利用しても再インストールを行うステップから繰り返してしまうため、インストールにかかる時間が発生してしまうためです。
代わりにbrew link
コマンドを利用することで、数分かかっていた処理を数秒レベルに短縮することができます。
steps:
- script:
inputs:
- content: |-
#!/usr/bin/env bash
envman add --key BREW_MINT --value "$(brew --cellar)/mint"
brew install mint
brew link mint
title: install mint
############################
steps:
- cache-push:
inputs:
- cache_paths: |-
$BREW_MINT
Mint / XcodeGen
MintでインストールしたライブラリやXcodeGenが生成したprojectファイルをキャッシュします
steps:
- cache-push:
inputs:
- cache_paths: |-
$MINT_PATH
$XCODEGEN_CACHE_DIR
############################
app:
envs:
- MINT_PATH: /usr/local/lib/mint
- XCODEGEN_CACHE_DIR: /Users/vagrant/.xcodegen/cache
Ruby Gem (Bundler)
Gemfile.lock
の変更を検知してキャッシュすることでBundlerでインストールしたライブラリをキャッシュします
steps:
- cache-push:
inputs:
- cache_paths: |-
$CACHE_BUNDLER -> ./Gemfile.lock
############################
app:
envs:
- CACHE_BUNDLER: $BITRISE_SOURCE_DIR/vendor/bundle
Swift Package Manager
Xcode 12.5から、Swift Package ManagerのRepositoryごとにキャッシュが保存できるようになりました! これまでは、ライブラリ全体のビルドキャッシュを保持することしかできなかったため、ライブラリ一つに変更が入るたびにFetchに失敗し、一からやり直さなければいけませんでしたが、12.5以降はSwift Package Managerが格段に安定するようになりました。
steps:
- cache-push:
inputs:
- cache_paths: |-
$SPM_CACHE_DIR
############################
app:
envs:
- SPM_CACHE_DIR: /Users/vagrant/Library/Caches/org.swift.swiftpm
並行処理
キャッシュを利用して毎回のビルドの時間を短縮化しました。 しかし、それでもワークフロー全体の処理が長くなってしまうため、いくつか並行処理を行って分散できないか検討してみました。
例えば、デプロイというワークフローを行う上で、以下のような4つのステップを想定してみます。
- テスト
- 簡易検証用にSimulator向けバイナリを生成してSlackに送信
xcodebuild archive
コマンドを用いて、提出用実機バイナリを生成- Testflightへデプロイ
これらを並行して行ってみます。そうすると、
- 並行して行いたい処理
- テスト
- 簡易検証用にSimulator向けバイナリを生成してSlackに送信
xcodebuild archive
コマンドを用いて、提出用実機バイナリを生成
- 1-1 / 1-2 / 1-3 の結果を待ち受けて、成功したら Testflightへデプロイ
このように、最初の3つの処理をCIの別のレーンを利用すれば理想的には作業時間を1/3に減らせそうです。 Bitriseはこのような別のレーンに処理を投げることが簡単に行えるようになっています。
_test_and_build_simulator_async:
steps:
- build-router-start:
inputs:
- workflows:
- test
- build_simulator_app
- access_token: $BITRISE_ACCESS_TOKEN
test:
steps:
- xcode-test: {}
build_simulator_app:
steps:
- xcode-build-for-simulator: {}
- deploy-to-bitrise-io:
inputs:
- is_enable_public_page: 'false'
- is_compress: 'true'
deploy:
before_run:
- _test_and_build_simulator_async
steps:
- xcode-archive:
inputs:
- export_method: app-store
after_run:
- _wait_other_workflows # 他のworkflowsを待ち受ける
- # デプロイする
- # Slack に通知する
_wait_other_workflows:
steps:
- build-router-wait:
inputs:
- abort_on_fail: 'yes' # 他のworkflowsがFailしてたらDeployは進めない
- build_artifacts_save_path: $BITRISE_DEPLOY_DIR/
- access_token: $BITRISE_ACCESS_TOKEN
このように、公式で提供されているbuild-router-start
とbuild-router-wait
ステップと共にBitriseのAPI AccessTokenを利用することで、Workflowを新たに投げて、その結果を待ち受ける処理を容易に記述することができます。
上記スクリプトでは、アーカイブする処理を本流として、「テスト・Simulator用のバイナリアップロード」を別のWorkflowに投げて、両者が成功したときのみTestflightへのUploadを続行するというロジックになっています。
CI上で複雑なことを行おうとすると管理が困難な状態に陥りがちですが、Bitriseでは公式がStepとして提供してくれているものを組み合わせるだけで利用することができ、とても便利です。 契約している価格プランを吟味しつつ、検討してみてください!
並行処理の結果を集約する
1-2. 簡易検証用にSimulator向けバイナリを生成してSlackに送信
Bitriseは別のWorkflowに任せて生成されたArtifactsを共有することができます。
収集したArtifactsをBitriseにdeployした上で、$BITRISE_PERMANENT_DOWNLOAD_URL_MAP
を参照することでダウンロード用のLinkを取得することが可能です。
_wait_other_workflows:
steps:
- build-router-wait:
inputs:
- abort_on_fail: 'yes'
- build_artifacts_save_path: $BITRISE_DEPLOY_DIR/ # 並行したworkflowのartifactsが保存
- access_token: $BITRISE_ACCESS_TOKEN
- deploy-to-bitrise-io:
inputs:
- deploy_path: $BITRISE_DEPLOY_DIR/deploy.zip # 上げたいファイルを指定
- is_enable_public_page: 'false'
- script:
inputs:
- content: >-
# `key(fileName) => value(url)` で格納されている
APP_DOWNLOAD_URL=$(echo $BITRISE_PERMANENT_DOWNLOAD_URL_MAP | cut -f 2 -d ">")
envman add --key APP_DOWNLOAD_URL --value "$APP_DOWNLOAD_URL"
- slack:
inputs:
- buttons: | # slackにLinkボタンを追加
Simulator App|${APP_DOWNLOAD_URL}
上記スクリプトでは、SlackのNotificationにSimulator用バイナリをDLできるボタンを用意することで、開発チームが簡易的にそのバージョンの動作確認を行うバイナリ配布を実現しました。便利ですね!

終わりに
iOSアプリチームではより良い開発体験へと刷新していくために開発環境の改善を行っています。 本記事ではその一端として、管理しやすいプロジェクト構成やCI環境の整備例を紹介しました。
明日は4日目、伊藤さんによる「日経電子版アクセシビリティの取り組み」です。お楽しみに!