Swift2.2 から Swift3 へ頑張って移行した話

Xcode7 + Swift2.2 から Xcode8 + Swift3 へマイグレーションしたことの作業記録です。同じようにステップアップする人は沢山いると思いますので何かの参考になればこれ幸いです。

というわけで、開発を担当している [Zaim](https://zaim.co.jp/) の iOS アプリで Swift のバージョンアップを実行しました。コンパイルエラーの掃除がなかなかに大変でしたよ。

# 移行のおおまかな流れ

> *”何事も最初が肝心なんだよ”*

だいぶ省略するけど以下みたいな感じか。移行できるかどうか調べるとかまあそういうのは置いといて…。

1. （誰かが Swift3 を使いたいと言いだす）
2. 使っている外部ライブラリをアップデートする
3. Xcode の Convert To Current Swift Syntax で Swift3 に変換してもらう
4. 変換ミスを修正したり、変換してくれななかった部分を修正する、外部ライブラリ類のアップデートに追従する
5. 動作テスト＆修正

[4] が大変でした。うちのプロジェクトは Swift ファイルが数百くらいの規模感なのでまあそこそこって感じでしょうか。ひたすらコンパイルエラーの修正をするという地味なことなんですけどなんだかんだ一週間ほどかかったという。


# 移行する前に

> *”石橋を叩いて渡ろう”*

## ライブラリの対応状況を確認する

Swift ベースの外部ライブラリを使っている場合はそれらが Swift3 に対応しているか調べないといけません。対応していなかったら、対応するまで待つか他のライブラリを探すか自分で対応してプルリクだすかを決めないといけません。

自分の場合はだいぶ前からアップデート内容を定期的にチェックしていて、全部のライブラリが Swift3 に対応したってことを確認してから移行に踏み切りました。

また、使っているライブラリがそもそも結構古いバージョンの場合は、Swift2.2 で動く最新のバージョンまでアップデートして動作確認をしてから移行するのが望ましいです。あまり数は多くないですが Swift3 対応のバージョンでそれまでとはインターフェイスが変わることがあります。

変更点が多いと問題が発生した場合にマイグレーションが影響しているのかライブラリの使い方がおかしいのか切り分けが難しくなります。一部例を挙げると、APIKit のバージョン1 系を使っていたのを 2.0.4 に上げてから Swift3 マイグレーション時に 3.0.0 へアップデートを行ないました。1 から 2.0.4 に上げたときと同じ不具合が Swift3 変換完了後に見つかったのですが、一度経験済みの内容だったので特に焦ることなくすぐに修正できました。1 系から一気にアップデートしていたら変更も多く、こうはいかなかったと思います。

## あらかじめテストを増やしておく

後述しますが Xcode でコンバートしたコードに間違いがあることがあります。手作業で修正した内容にミスが入ることも普通にあるでしょう（ミスしない人は居ないよ）。そんなときにちゃんとしたテストがあれば機械的にミスを発見してくれるのでかなり心強いです。

今回もテストがあったおかげでロジックの修正ミスをサクッと発見することができてテストの重要性を再認識しました。

# 移行をはじめよう

> *”どんなことにも終わりはある”*

## Xcode のコンバータについて

古いプロジェクトを Xcode で開くと新しいシンタックスへコンバートしてくれます。Swift2.3 は試してないので知らんのですが、Swift3 の場合は結構変更が大きいので変換漏れはたくさんでます。

基本的にコンパイルエラーが無数に湧き出てくるのですべて解消するまでは動作テストもできません。ひたすらこれらを修正していくのがメインの作業になります。

変換漏れで代表的なのは「enum の先頭文字が小文字に変換されたものの、実際の利用箇所では変換されてない」というパターンです。どういう理屈で漏れてるのかよくわかりませんが、これはすごい多いです。特に外部ライブラリの enum はあまり変換されている印象はありませんでした。あとはメソッド名が古いシンタックスのままだったりとか。

変換ミスは多くありませんが、間違った変換をされるのは致命的です。めんどくさいですけど自動変換した内容は複数人でチェックしたほうがいいでしょう。実際に自分が遭遇した例をひとつ挙げますが…これは ReactiveCocoa のコードで next に書いた処理が消えました。

```swift
    .on(
        failed: { [weak self] _ in
            print("ログインに失敗しました")
-           let alert = UIAlertController(title: "失敗", message: "ログインに失敗しました", preferredStyle: .Alert)
+           let alert = UIAlertController(title: "失敗", message: "ログインに失敗しました", preferredStyle: .alert)
            alert.addDefault("OK")
-           self?.presentViewController(alert, animated: true, completion: nil)
-       },
-       next: { [weak self] response in
-           self?.km.add(oauthToken: token, oauthTokenSecret: secret)
+           self?.present(alert, animated: true, completion: nil)
        },
```

## コンパイルエラーの簡単な修正方法

コンパイルエラーはそれはもう湯水のように次から次へと湧いてでてきます。ただ、以下のような赤丸のコンパイルエラーの場合はクリックするとよしなに修正してくれます。便利です。

<img width="332" alt="スクリーンショット 2016-10-27 19.21.35.png" src="https://qiita-image-store.s3.amazonaws.com/0/3336/d5df9146-ca0c-43a3-1516-e38ddc79f660.png">

以下のようなビックリマークは見るのも嫌になりますが諦めて手作業で修正します。

<img width="50" alt="スクリーンショット 2016-10-27 19.27.17.png" src="https://qiita-image-store.s3.amazonaws.com/0/3336/9d0a194f-22c2-bdd3-0e3e-112cd1520abb.png">

これは途中で教えてもらったんですが、Xcode の Editor メニューに `Fix All in Scope` という項目があってこれを選択するとファイルの赤丸を一気に全部なおしてくれます。こいつはすげえ便利ですので活用しましょう。

## 共同開発をしている場合

ひとりでアプリの開発をしているんなら何も気にせずズバッとコンバートしてもくもくとコンパイルエラーを修正すればいいんですが、残念ながら複数人で共同作業をしている場合は他の人の作業への影響をなるべく小さくしないといけません。

そこで自分が取った方法は以下です。

1. メインブランチから手元にマージ
2. Xcode でコンバートする（うちの場合は15分くらい）
3. 変換漏れのなかから単純な文字列置換で変換できそうなものは sed コマンド用に置換ルールを作っておく
4. ロジックの修正が必要なものは修正＆こまめにコミットをする
5. 修正がすべて片付いたら動作チェック＆修正

実際に [5] に到達するまで自分は1週間かかりました。当然このあいだにメインブランチはめっちゃ進みます。当たり前ですね。もちろんそいつらをマージすると完膚なきまでにコンフリクトします。しかしながら移行が完了したあとにマージするという選択肢は…想像するだけで恐ろしいです。

なので、メインブランチが進んでいたらある程度キリのいいところで手元に反映させないといけません。

* 新しくメインブランチをベースにしたブランチを作成
* Xcode でコンバートを実行
* [3] で作った sed の置換を適用
* [4] の修正コミットを `git cherry-pick` で反映

という感じでひたすら作業を進めました。[3] を作るのとか大変めんどくさいですが、メインブランチから作業をリスタートした場合にコンフリクトがでるとかったるそうなのでこういう進め方をしまいた。あとは自分が突然死んだ場合にもコンフリクトが少ないほうが作業が再現しやすいんじゃないかなと。


## sed のサンプル

別に置換してくれるなら Ruby でも Perl でもなんでもいいんですが。参考までに。

```zsh
#! /bin/zsh

sed -i "" \
    -e 's/\.Default(/.default(/g' \
    -e 's/\.Alert/.alert/g' \
    -e 's/\.Success/.success/g' \
    -e 's/\.PUT/.put/g' \
    -e 's/\.GET/.get/g' \
    -e 's/\.POST/.post/g' \
    -e 's/\.DELETE/.delete/g' \
    Zaim*/**/*.swift
```

## swiftgen が完全じゃない

[swiftgen](https://github.com/AliSoftware/SwiftGen) というのを使っています。これは画像のファイル名とか Storyboard の identifier とか本来なら文字列で扱う引き数をコードで扱えるようにしてくれます。うちのプロジェクトだと Storyboard 用の機能で使っています。

swiftgen 自体は Swift3 対応をうたっていて、 `swiftgen storyboards -t swift3 ...` とすると Swift3 で使えるコードを吐いてくれます。ただ残念なことに、 `performSegue` のコードが overload 的にちゃんと認識されなくてそのままだと動かないようで、自分は暫定的に以下の Extension を追加しました。issue もあるので本体の対応待ちです。→ https://github.com/AliSoftware/SwiftGen/issues/174

```swift
extension UIViewController {

    func performSegue<S: StoryboardSegueType>(_ segue: S, sender: Any? = nil) where S.RawValue == String {
        performSegue(withIdentifier: segue.rawValue, sender: sender)
    }

}
```

もうひとつ問題があり、Xcode のコンバータは String タイプの enum の文字列の先頭は小文字に変換してくれません。ですが swiftgen の生成する enum は Swift3 の仕様に合わせて先頭が小文字になります。

つまり Xcode は swiftgen 用のコード `performSegue(StoryboardSegue.PremiumService.WebView)` みたいなやつの `WebView` の部分を `webView` に書き換えてくれないのですが swiftgen は容赦なく `webView` の書式でコードを吐くのでコンパイルエラーが山盛りとなって襲いかかってきます。

しかもこの時のエラーが performSegue に `withIdentifier:` を追加しろよっていう赤丸のエラーなんでうっかりクリックしちゃうんですが対応としては間違ってるんで困りもんです。

正直この不整合の解消はなかなかやっかいで、自分の場合はある程度コンパイルエラー修正の目処がつくまでは上記の extension を追加するのに抑えて swiftgen を使うのを一時的に止めておきました。

修正が終わったら `swiftgen -t swift3` で反映し、手作業で enum の修正をしました。適当にスクリプト書いたらある程度機械的に変換もできるでしょうが Attributes を追加してあげたら Xcode が認識してくれるんで Fix All して修正していきました。

```swift
case edit = "Edit"
@available(*, unavailable, renamed: "edit")
case Edit = "deprecated-Edit"
```

試してないのでわからないんですが、Xcode のコンバートをする前にこれらのコードを用意しておいたらコンバート時に自動で変換してくれるかもしれません。

## なんで一週間もかかったし？

正直よくわからん。コミットログ見返してもあんまり量が多くないんだよ。たぶん自分がクソだからですね本当にありがとうございました。

とは言え「Swift2.2 環境でのライブラリのアップデート〜自前で別途 Framework 化しているライブラリのアップデート〜本体のアップデート」という内容だったのでそれなりにボリュームはあったんじゃないかなと。

あとはまぁ…たぶんライブラリのアップデート対応をするのにいちいち内部のコードとかコミットログとか見に行ったりしてたから、その辺は結構時間をロスする原因だったろうに思う。一部のライブラリは~~わけのわからん~~アグレッシブに破壊的な変更を加えてきていてそもそも諦めた部分もあったし。

# Xcode8 について

> *”俺たちの戦いはこれからだ ”*

以下は移行完了時に同僚宛に書いた内容の抜粋です。参考になれば。

## デバッグ実行するとログがうざすぎワロタ

Xcode8 だとバグがあってログが大量に出ます。抑制する方法は以下を参照ください。

- [Xcode 8 の不要なログを非表示にする方法](http://qiita.com/plasticstraw/items/77d93cdb644dc66c48f8)

※この設定をすると AutoLayout の警告が表示されなくなるそうなので気をつけてください → [AutoLayoutのデバッグをする（1）](http://qiita.com/akatsuki174/items/6935bb6ff64f4df51c5a#%E3%81%8A%E3%81%BE%E3%81%91%E6%B3%A8%E6%84%8F)

## Xcode8 だとシミュレータで Keychain が使えないらしい

これもバグで `Capabilities` から `Keychain Sharing` をオンにすると解決するらしいです…が、オフのままでも動作してるように見えるのでこの設定はしていません。バグなおったのかな？

## 今後のコーディングについての参考情報

Cocoa の API を使うときに「これまでと違くてわかんねーし！」ってなることは多いと思います。そんなときは Apple の API Diff が参考になります。↓ とか結構なんども参照しました。

- https://developer.apple.com/library/prerelease/content/releasenotes/General/iOS10APIDiffs/
- [UIKit](https://developer.apple.com/library/prerelease/content/releasenotes/General/iOS10APIDiffs/Swift/UIKit.html)
- [Foundation](https://developer.apple.com/library/prerelease/content/releasenotes/General/iOS10APIDiffs/Swift/Foundation.html)