Xcode
iOS
Carthage

iOS開発における最強のパッケージ管理方法

この記事では私が最強だと思っているiOS開発におけるパッケージ管理方法を紹介します。
ここで言うパッケージ管理とは、我々がアプリやライブラリを開発する際において、
依存する外部ライブラリを宣言、取得、ビルド、共有等をすることです。

最強の方法

この記事で紹介する最強の方法は、「Carthage --no-build --use-submodule + xcworkspace」方式です。
その名の通り、Carthageを--no-build --use-submoduleオプションと共に使用しつつ、xcworkspaceを使います。
以下ではその詳細について述べます。

そもそもパッケージ管理とは何か

我々がパッケージ管理に求めている事は何でしょうか。
私は大きなところでは下記だと整理しています。

  • 依存するライブラリのバージョンを宣言・共有できる事
  • 依存ツリーをフラット化して解決できる事

依存するライブラリのバージョンを宣言・共有できる事

あるライブラリやアプリ(以下「プロジェクト」と呼ぶ)を開発する際において、使用するライブラリのバージョンを管理する必要があります。
そして、そのプロジェクトを共有・共同開発する際に、どのホストでも使用するライブラリが同一になるようにします。

例えば良くない管理方式として、
使用するライブラリがホスト環境にグローバルインストールされる構成になっていると、
ホスト環境によって使用するライブラリのバージョンが異なってしまうため、
プロジェクトが壊れる恐れがあります。

最近のパッケージ管理ツールでは、プロジェクトローカルに利用するライブラリを宣言して、
開発時においてもプロジェクトローカルにライブラリを取得する事でこれを実現しています。

これだけだったらgitのサブモジュール機能で実現できます。
プロジェクト管理にはすでにgitを使用しているでしょうから、
そのままgitだけで完結できると便利です。
しかし、実際のところうまくいきません。

依存ツリーをフラット化して解決できる事

パッケージ管理では依存ツリーをフラット化して解決する必要があります。
例えば、とあるライブラリAが世にあったとします。
また、Aに依存するライブラリBとCがあったとします。
そして、アプリDの開発者は、ライブラリBとライブラリCの両方を使用したくなったとします。

すると、アプリDはBに依存しているので、間接的にAにも依存しています。
一方、アプリDはCにも依存しているので、やはり間接的にAに依存しています。
依存ツリー上は2つのAが存在していますが、ビルドの際にはAは1つしか存在できないため、
アプリDの開発者はBとCの両方に向けて、単一のAを用意する必要があります。
その際に、Aのどのバージョンを使用すればよいのか、正解を得る必要があります。

場合によっては、ライブラリBとCは同時に使用することができない、という事もあります。
例えばBはAのバージョン1.1を要求、CはAのバージョン1.2を要求している、というように、
要求が相互に矛盾してしまう場合です。

ここで例えばもし、Bの要求するAのバージョンが1.1ちょうどではなく、「1.1互換な何か」であったならば、
Aの1.2を使用すればBとCの両方の要求を満たすことができます。
このことから、依存の宣言はちょうどのバージョン指定だけではなく、
互換性を考慮して条件を緩和することもできるようになっていると便利です。

Carthageなどのパッケージマネージャではこのような依存宣言と、
その依存ツリーの解決を行ってくれます。

その他の要求

ここまでの内容によれば、Carthageを使用すれば良さそうですが、
Carthageの通常の使用方法では下記のような問題が残ります。

  • 不要なビルド待ちの時間がある
  • ライブラリのソースの変更が困難
  • App Store申請時に問題が発生する恐れがある

不要なビルド待ちの時間がある

Carthageではライブラリの対応しているプラットフォームの種類の分だけライブラリのビルドを行います。
多くの場合ではmacOS, tvOS, watchOS向けのビルドは不要です。
これらはコマンドラインオプションでスキップすることができますが、
たとえiOSだけであっても、armv7, arm64, i386, x86_64の4つのビルドが行われます。

プロジェクトの作業中において、
もし64bitのiPhone実機を接続して作業しているなら、
本来的にはarm64だけで良いはずです。
もし64bitのiPhoneシミュレータで実行しているなら、
本来的にはx86_64だけで良いはずです。

App Storeに申請するときにはarmv7とarm64の両方が必要で、一方でi386とx86_64は不要です。
もし含まれているとストアのシステムがエラーを返します。

CarthageではXcodeでの開発作業とは独立に事前にビルドする設計になっているために、
とりあえずすべてビルドしておく設計になってしまっています。
そして、copy-frameworkフックを用いて、
申請に際してはシミュレータ向けのバイナリを取り除くという方式になっています。

この点について、Xcode自体の機能を使用すれば、
必要なビルドだけが行われるようにプロジェクトを構築できます。

また、プロジェクトをデバッグビルドしている際はライブラリもデバッグビルド、
リリースビルドしている際はライブラリもリリースビルドされます。

ライブラリのソースの変更が困難

ライブラリをデバッグしている際、試しにソースを変更したりできると便利です。
自分でライブラリを作成していて、プロジェクトと同時にライブラリも編集している場合にも、
ライブラリのソースが変更できると便利です。
Xcodeでプロジェクトを組んでおくと、シームレスに作業ができて楽です。
ライブラリのソースを変更しない限りは、
Xcode側でビルドキャッシュ機構が動作するので、余計なリビルドも生じません。

App Store申請時に問題が発生する恐れがある

前述したように、Carthageではシミュレータバイナリを後から取り除いて申請するのが
標準となっていますが、このような自前のビルド機構は、
AppleによるApp Storeの仕様変更に対して不安定です。

これまでにもAppleはbitcode対応や64bit対応など、
バイナリに関する要求を更新してきました。

こうした変更への対応は、最新のXcodeを使えば簡単に解決できることが多いですが、
Carthageのような自前ビルド機構が問題を起こす場合があります。
多くの場合はCarthage側で対応が入ると思いますが、
自分が踏んでいる問題が何らかのコーナーケースになっていて、
コミュニティが感知していないなどの恐れもあります。
そのような場合に、自力でCarthageに手を入れて問題を回避するのは難易度が高いです。

App Storeへの申請がある事を考えると、
iOSプロジェクトはなるべく純正のXcode環境で構築したほうが、
トラブルを未然に回避できると思います。

Carthage --no-build --use-submodules + xcworkspace 方式

ここまで説明したとおり、
パッケージ管理にはCarthageを使用しつつ、
ビルド関係はXcodeで構築するのが最強だと思います。

その構成手順について説明します。

プロジェクトのライブラリを管理する人は、通常どおりCartfileを作成、編集します。
Cartfileを編集したら、下記コマンドで取得します。

$ carthage update --no-build --use-submodules

--no-buildオプションがついているので、バージョンの解決とチェックアウトだけが行われます。
--use-submodulesオプションがついているので、チェックアウトされたライブラリのリポジトリが、
git上でサブモジュールとしてステージされます。

このプロジェクトを取り扱うけれども、
依存ライブラリの更新は行わない人は、
Carthageを使用する必要はありません。

プロジェクトをチェックアウトした後、下記コマンドにてサブモジュールを取得すれば、
依存ライブラリのチェックアウトができます。

$ git submodule update --init

この方法の良いところとして、
ライブラリの更新をしない作業者は、Carthageを使わなくてもよいという点と、
もしライブラリがCarthageに対応していなくても、
git submoduleに対応していれば同じ枠組みでとりあつかえるという点もあります。
ただしgit submoduleで管理する場合、前述の通り手動での依存ツリー解決が必要ですが。

ビルドの構成においてはXcodeのWorkspaceという機能を使います。
WorkspaceはXcodeのProjectを複数登録できるだけのデータ構造です。

File > New > Workspace から作成できます。拡張子は.xcworkspaceです。
以後紛らわしいのでxcworkspaceと呼びます。

開発するアプリやライブラリそれ自体は通常通りXcodeのProjectを使います。
これは拡張子.xcodeprojです。以後xcodeprojと呼びます。

このアプリのxcodeprojをxcworkspaceに登録します。

以降の作業は必ずxcworkspaceをXcodeで開いて行います。
注意点として、xcworkspaceに登録されているxcodeprojは、
xcworkspaceを開いているとxcodeprojがそのxcworkspaceによって専有されて開かれている状態になり、
そのxcodeprojを別のXcodeウィンドウで直接開いたり、
別のxcworkspace経由で同時に開くことはできません。

アプリのxcodeprojが追加できたら、
次は依存ライブラリのxcodeprojをxcworkspaceに追加します。

Carthageによって Carthage/Checkouts/<Library>/<Library>.xcodeproj がチェックアウトされているので、
これを登録します。

次に、アプリのプロジェクトにライブラリへの依存を設定します。
この手順に若干癖があります。

まず、xcworkspaceの左のプロジェクトツリーペインの中の、アプリのxcodeprojをクリックします。
アプリのターゲットを選択して、Generalタブを開きます。
画面下部にある Linked Frameworks and Libraries の + ボタンをクリックします。

Choose frameworks and libraries to add: と書かれたダイアログが開くので、
Workspaceの中の <Library>.framework を選択し、 Add ボタンをクリックします。

すると、左ペインのアプリのxcodeprojのツリーの中に、
FrameworksというFolder無しのGroupが作られ、その配下に <Library>.framework が追加されます。

次に、その現れた <Library>.framework を、 Embedded Binaries の中にドラッグアンドドロップします。
すると、なぜか Linked Frameworks and Libraries の中にもう一つ <Library>.framework が追加され2つになるので、
片方を削除します。

これで依存関係の構成は完了です。

念の為以下を確認します。

Frameworksの配下の<Library>.frameworkをクリックして、
右ペインの Identity and Type の Location が Relative to Build Products になっている事を確認します。

最後に、不要なSchemeを非表示にします。
Carthageに対応したライブラリの場合、1つのライブラリプロジェクトについて、
対応OSごとにSchemeが設定されていて、それがすべてxcworkspaceに取り込まれます。
例えばRxSwiftなら、Schemeのところに、下記すべてが列挙されます。

RxBlocking-iOS, RxBlocking-macOS, RxBlocking-tvOS, RxBlocking-watchOS,
RxCocoa-iOS, RxCocoa-macOS, RxCocoa-tvOS, RxCocoa-watchOS,
RxSwift-iOS, RxSwift-macOS, RxSwift-tvOS, RxSwift-watchOS,
RxTests-iOS, RxTests-macOS, RxTests-tvOS, RxTests-watchOS,
Microoptimizations, Benchmarks

これを Manage Schemes メニューを開いて、不要なものについては Show チェックボックスを外すことによって、
普段はXcode上から非表示にできます。

なお、この Show チェックボックスをクリックすると、
稀にライブラリの作られたXcodeと、使っているXcodeのバージョンが違うと、
ライブラリ側のSchemaに関するファイルに変更が発生してしまうことがあります。
その場合、新しいXcodeで構築した状態でプルリクエストを投げて直してもらうか、
単に無視するのが良いです。

--use-submodules を使わない選択肢も有り

git submoduleを使いたくない場合、 --use-submodules は使わなくても良いです。
その場合は、共同開発者はライブラリをチェックアウトするために
$ carthage checkout をすれば良いです。
それ以外のビルドの構成等は同様にできます。

Carthage