iOS 7 でも Carthage を使おう

本投稿では Swift のライブラリ管理に欠かせない [Carthage](https://github.com/Carthage/Carthage) について、

1. __Carthage の基本的な使い方__
2. __iOS 7 での Carthage の使い方__

を説明します。

## はじめに

ライブラリの依存関係を解決し管理するのは大変な作業です。 Swift にはライブラリの依存管理ツールとして主に [Carthage](https://github.com/Carthage/Carthage) と [CocoaPods](https://cocoapods.org/) という選択肢があります。

__Carthage はとてもシンプルな仕組みで動いているので、余計なファイルが作られたり変な制約が生まれたりしません。できるだけシンプルな状態でリポジトリを保ちたい人に向いています。__ Carthage と CocoaPods の比較は[こちらの投稿](http://qiita.com/nori0620/items/b81ae171f0e82b0c2d8a)（または、その翻訳元の英文である [Carthage の README](https://github.com/Carthage/Carthage#differences-between-carthage-and-cocoapods) ）が詳しいです。


もし今 Git のサブモジュールを使ってライブラリを管理しているのであれば、 Carthage を使うことで幸せになれると思います。サブモジュールをでライブラリを扱うシンプルさ（何が起こっているのか自分ですべて把握し管理できるという意味で）と、コマンド一つでライブラリの依存関係を解決して checkout できる便利さを両立できます。

しかし、、 Carthage は iOS 8 以降でのみサポートされている Embedded Binaries を前提としており、README にも

> Note that Carthage only supports dynamic frameworks, which are only available on iOS 8 or later

と書かれています。現実的にはまだ iOS 7 を切り捨てづらいことから、 Carthage は使ってみたいけどあきらめているという人も多いかもしれません。しかし、これは iOS 8 でないと Carthage を使えないことを意味しません。 __実は iOS 7 でも Carthage を十分に活用することができます。__

本投稿ではまず Carthage の使い方について説明し、その後、 iOS 7 で Carthage を使う方法について説明します。

## Carthage の基本的な使い方

### インストール

Carthage は [Homebrew](http://brew.sh/index_ja.html) を使って簡単にインストールできます。

Homebrew とは Mac でデファクトになっている（と思う）パッケージマネージャで、ターミナルで↓のコマンドを実行するだけでインストールできます（ MacPorts を使っていると競合するので気をつけて下さい）。

```bash
ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
```

Homebrew を使えば、次のコマンドで Carthage をインストールできます。

```bash
brew install carthage
```

もし、後日 Carthage のアップデートを反映したければ次のコマンドを実行するだけです。

```bash
brew upgrade carthage
```

### Cartfile

Carthage では、 __Cartfile__ と呼ばれるテキストファイルにインストールしたいライブラリを記述します。

例えば、 Alamofire の 1.3 以降の最新リリースと、 Swifty JSON の 2.0 以降の最新リリースをインストールするには次のように書きます。とても簡単ですね。

```:Cartfile
github "Alamofire/Alamofire" >= 1.3
github "SwiftyJSON/SwiftyJSON" >= 2.0
```

`Cartfile` を書いたら次のコマンドを実行すると、ライブラリが checkout されてビルドされます。

```bash
carthage update
```

コマンドが完了したら `Carthage` というディレクトリができているので見てみましょう。  `Carthage` ディレクトリの中身は次の通りです。

- `Carthage/Checkouts`: リポジトリを clone した結果
- `Carthage/Build`: ビルドされた Framework

通常は `Carthage/Build` の中の Framework を Embed して使います（ iOS 8 以降で標準の Framework の使い方です）。 iOS 7 については後述します。

#### GitHub 以外のリポジトリの指定方法

上記の `Cartfile` では GitHub のリポジトリを指定していまが、 GitHub 以外の Git リポジトリを指定することもできます。 `github` の代わりに `git "https://foo.orf/bar.git"` にように書けば、任意の Git リポジトリを対象とすることができます。

#### バージョンやブランチ、特定のコミットの指定方法

`Cartfile` の `>= 1.3` の部分はバージョンを指定しているのですが、 [Carthage の Documentation](https://github.com/Carthage/Carthage/blob/master/Documentation/Artifacts.md#version-requirement) にある通り、その他にも様々な指定が可能です。

> 1. >= 1.0 for “at least version 1.0”
> 2. ~> 1.0 for “compatible with version 1.0”
> 3. == 1.0 for “exactly version 1.0”
> 4. "some-branch-or-tag-or-commit" for a specific Git object (anything allowed by git rev-parse)

`>=` を使えば、指定したバージョン以降の最新のバージョンが、 `~>` を使えば指定したバージョンと __互換性がある__ 最新のバージョンが、 `==` を使えば指定した特定のバージョンがインストールされます。

それ以外に、 4 で書かれているように特定のブランチやコミットを指定することもできます（下記は [PromiseK](https://github.com/koher/PromiseK) の `experiment` ブランチを指定する例です）。

```:Cartfile
github "koher/PromiseK" "experiment"
```

ブランチの代わりにコミット番号を書けば特定のコミットを指定することができます。

#### Semantic Versioning

`~>` を使えば指定したバージョンと互換性がある最新のバージョンがインストールされると書きましたが、互換性はどのように検証されているのでしょうか。実は、バージョンの付け方に秘密があります。

Carthage 対応のライブラリは [Semantic Versioning](http://semver.org/) というバージョンの付け方をする必要があります。 Semantic Versioning では、 `1.2.3` というバージョンがあるとすると、 `1` の部分が __MAJOR version__ 、 `2` の部分が __MINOR version__ 、 `3` の部分が __PATCH version__ と呼ばれます。

互換性のない変更が行われたときには MAJOR version を、 API が追加されるだけなど互換性がある変更が行われた場合には MINOR version を、（互換性のある）バグフィックスなどの場合は PATCH version を更新するというルールでバージョンが付けられるので、 Semantic Versioning に従っているという前提であれば、バージョンを調べるだけで互換性の有無がわかるのです。

#### コミットとバージョンの関連付け

__どのコミットがどのバージョンであるかは Git のタグで管理されます。__ 例えば、 `1.2.3` のようにタグを付けておくだけで、 Carthage はそのコミットが `1.2.3` というバージョンなんだと認識してくれます。

### Cartfile.resolved

`carthage update` を実行すると、 `Carthage` ディレクトリとは別に __Cartfile.resolved__ というファイルが生成されます。

このファイルには、 `Cartfile` に基いて __実際にチェックアウトされたバージョン（タグ）／ブランチ／コミット__ が記載されています。例えば、下記が `Cartfile.resolved` の一例です。

```:Cartfile.resolved
github "Alamofire/Alamofire" "1.3.0"
github "SwiftyJSON/SwiftyJSON" "2.2.0"
```

`Cartfile` の中の `>=` の記述が消え、特定のバージョンが指定されていることがわかります。また、 SwiftyJSON では `>= 2.0` を指定していましたが、互換最新リリースである `2.2.0` が checkout されたことがわかります。

この `Cartfile.resolved` は `Cartfile` を元に生成されるもののため、 ignore してしまうべきもののように思えるかもしれませんが、 __`Cartfile.resolved` を ignore してはいけません__。

Carthage には `carthage update` とは別に下記のコマンドが用意されています。

```bash
carthage bootstrap
```

これを使えば `Cartfile` から適切なバージョンが選択されるステップを省いて、 `Carthage.resolved` で指定されたバージョンがそのまま checkout されます。

__`carthage update` はライブラリの更新を管理する人だけが実行するようにし、その他のメンバーは `carthage bootstrap` を実行するようにすることで、全員が同じバージョンのライブラリを使っていることを保証することができます。__

そのため、 `Cartfile` に加えて `Cartfile.resolved` も ignore せずにコミットに含んでおく必要があるわけです。

### .gitignore

Carthage を使う場合の `.gitignore` はどのように書けば良いでしょうか。基本的には [github/gitignore](https://github.com/github/gitignore) の [Swift.gitignore](https://github.com/github/gitignore/blob/master/Swift.gitignore) を使えば良いです。

しかし、この `Swift.gitignore` では次のように `Carthage/Checkouts` ディレクトリがコメントアウトされており ignore されません。

```
# Carthage/Checkouts
```

前述のように、 `Carthage.resolved` があればライブラリの特定のバージョンを checkout できるので、基本的には `Carthage/Checkouts` をコミットに含める必要はありません。僕は `Carthage/Checkouts` を ignore するように変更して（ `#` を消して）使っています。

ただ、リポジトリにアクセス不能になったり force push されたりといった要因で、今の状態をいつでも完全に復元できるとは限りません。それが許容できないようであれば `Carthage/Checkouts` を ignore しないという選択肢もあります。

## iOS 7 での Carthage の使い方

iOS 7 では Embedded Binaries を使うことはできません。サブモジュールとしてライブラリを追加し、ソースを直接プロジェクトに追加することが多いと思います。

しかし、それであれば別にサブモジュールでなくてもいいのです。これまで見てきたように、 `Carthage/Checkouts` にはライブラリのソースが　checkout されているので、 __サブモジュールのときと同じように `Carthage/Checkouts` のソースをプロジェクトに追加すればいいのです。 `Carthage/Build` を使う必要はありません。__

そうすれば、 iOS 7 でもライブラリの checkout や依存関係の解決は Carthage に任せることができます。自分でわざわざサブモジュールを管理する必要はありません。

### Carthage に対応していないライブラリを Carthage で管理する

`Carthage/Build` を使わないのであれば、どんな Git リポジトリでも Carthage で管理することができます（ただし、依存関係の解決まではできません。実際には他のライブラリに依存していないライブラリも多いので、それでも十分役に立ちます）。

例として、僕が作った [CGPoint+Vector](https://github.com/koher/CGPoint-Vector) という、 Carthage 非対応のライブラリを Carthage で checkout してみましょう。 `Cartfile` に次の行を足して `carthage update` します。

```:Cartfile
github "koher/CGPoint-Vector" "master"
```

Carthage に対応していないのでビルドには失敗しますが `Carthage/Build` は使わないので問題ありません。 `Carthage/Checkouts` にはちゃんとリポジトリが clone されています。あとは、必要なファイルをプロジェクトに直接追加すれば OK です。

また、 `Cartfile.resolved` が一度作られれば、 `carthage update` や `carthage bootstrap` の代わりに次のコマンドを使うことができます。

```
carthage checkout
```

このコマンドはライブラリのビルドは行わないので、 `Carthage/Build` を使わない場合にはこちらを使うと良いでしょう。

### Swift 2.0 / Xcode 7 & iOS 7 & Alamofire

Swift で最も有名なライブラリの一つである [Alamofire](https://github.com/Alamofire/Alamofire) ですが、 Swift 2.0 / Xcode 7 かつ iOS 7 で使おうとすると次のような問題が起こります。

Swift 2.0 / Xcode 7 用には、 Alamofire の `swift-2.0` ブランチを使います。 Cartfile には次のように書きます。

```:Cartfile
github "Alamofire/Alamofire" "swift-2.0"
```

`carthage update` して `Carthage/Checkouts/Alamofire/Source` ディレクトリ下の swift ファイルをプロジェクトに追加すればいいのですが、ビルドしてみると次のようなエラーが発生します。

```
Carthage/Checkouts/Alamofire/Source/Request.swift:238:55: 'NSQualityOfService' is only available on iOS 8.0 or newer
Carthage/Checkouts/Alamofire/Source/Request.swift:238:36: 'qualityOfService' is only available on iOS 8.0 or newer
```

これは、 `swift-2.0` ブランチでは Deployment Target が iOS 8.0 になっており、 iOS 7.0 では使えない API が[ここ](https://github.com/Alamofire/Alamofire/blob/94d1128810e497558cea90c7bfc5ecabc42817a3/Source/Request.swift#L209)で使われているからです。同じくそれらの API が使えない OSX 10.9 は Deployment Target になっており、そのために[その直前の行](https://github.com/Alamofire/Alamofire/blob/94d1128810e497558cea90c7bfc5ecabc42817a3/Source/Request.swift#L208)で `#available` を使ってバージョン判定をしているのでそこに `iOS 8.0` を足してやれば解決します。

仕事で Swift 2.0 / Xcode 7 かつ iOS 7 で Alamofire が必要だったので、 Alamofire を fork して Deployment Target を 7.0 に変更し、 `#available` に `iOS 8.0` を足したバージョンを [koher/Alamofire](https://github.com/koher/Alamofire) の `swift-2.0` ブランチにコミットしてあります。とりあえず使いたい人は↓のようにすれば使えます。

```:Cartfile
# Swift 2.0 / Xcode 7 かつ iOS 7 で Alamofire を使いたい場合
github "koher/Alamofire" "swift-2.0"
```

まだ十分に確認ができていないので、確認ができたらプルリクを送るかもです。