既存開発と iPhoneX 対応を両立するために、あえてビルドを失敗させる

この記事は Tech KAYAC Advent Calendar 2017 の5日目の記事です。

毎回誰にも共感されない

techblog.kayac.com

techblog.kayac.com

な記事を書いております Lobi で iOS アプリを直している @Gemmbu です。

ところで iPhoneX が出てもう一ヶ月経ちますがどうでしょうか? iPhoneX 対応進んでいますか?というわけで Lobi iOS アプリをいち早く iPhoneX 対応させたテクニックのお話をします。

この記事の対象者

  • iPhoneX にまだ対応できていないそこのあなた
  • それだけじゃなく iPhoneX に対応できているそこのあなたも

iPhoneX 対応って上下画面するだけでしょ?

って iOS アプリ開発者じゃないならそう思うかもしれませんが、残念なことにそうではないのです。まず iPhoneX 対応アプリを出すには Xcode のメジャーバージョンを上げなければなりません。で、 Xcode のメジャーバージョンを上げるとはどういうことかというとアプリが使用するライブラリのバージョンが一斉に上がるということになります。つまり iPhoneX 対応するってことは iPhoneX 対応のコードを追加すると同時に、既存の iPhone 端末および過去の iOS で正しく動作することも確認する必要があります。サーバで例えるなら Ruby のバージョンを上げたり、 Ruby on Rails のバージョンを上げたりっていうのをドキュメントはあるけどぎりぎりまでコミュニティなしでやらなければいけない感じです。

なので、 iPhoneX 対応(というか Xcode メジャーバージョンを上げること)はスケジュールが読みづらい作業であり、その間他の開発に取りかかれなくなるリスクのある作業となっています。そのため多くの現場では新機能対応と iPhoneX 対応の板挟みになっていることだと思います。

開発と iPhoneX 対応を両立するために Lobi チームではどうしたか

  • 既存開発メンバと iPhoneX 対応メンバに分けた
  • 既存開発メンバ用のブランチと iPhoneX 対応メンバ用のブランチを分けた
  • iPhoneX 対応メンバ用のプランチに適当なタイミングで既存開発メンバ用のブランチを都度マージした
  • 既存開発メンバは Xcode8.3.3 ( iPhoneX 未対応の最終版)で開発した
  • iPhoneX 対応メンバは Xcode9.x (そのときどきの最新版)で開発した
  • 既存開発メンバの書いたコードは iPhoneX 対応メンバが全てレビューをした
  • 既存開発メンバの書いたコードで iPhoneX 対応が必要なコードは Xcode9.x 環境ではビルドを失敗させるような修正をレビューで行った

この中の取り組みで面白いのはXcode9.x 環境ではビルドを失敗させるような修正です。どのようにすればよいのでしょうか?

どのようにしてビルドを失敗させる?

以下のコードは iOS11 では UIViewController の topLayoutGuide/bottomLayoutGuide は deprecated なので iOS11 から使える新しいレイアウト管理のための safaArea に書き換える必要があります。

self.tableView.contentInset = UIEdgeInsetsMake(self.topLayoutGuide.length, 0, self.bottomLayoutGuide.length, 0);
self.tableView.scrollIndicatorInsets = self.tableView.contentInset;

まずはビルドを失敗させるように仕込みましょう。そのために #error directive を使用してコンパイル時にエラー発生させ、ビルドを失敗させます

#error topLayoutGuide/bottomLayoutGuide is deprecated in iOS11
self.tableView.contentInset = UIEdgeInsetsMake(self.topLayoutGuide.length, 0, self.bottomLayoutGuide.length, 0);
self.tableView.scrollIndicatorInsets = self.tableView.contentInset;

これで、ビルド時に topLayoutGuide/bottomLayoutGuide is deprecated in iOS11 とメッセージを出して失敗します。ただこの場合だと、 Xcode のバージョンに関係なくビルドが失敗しますので次に Xcode のバージョンチェックを挟みましょう。

まず XCODE_VERSION_MINOR という丁度良い環境変数が定義されているので、それを使います。そのため Preprocesser MacrosHOGEHOGE_XCODE_VERSION_MINOR=0x$(XCODE_VERSION_MINOR) を追加します*1

Preprocesser Macros に定義することでどこでも使用することができるようになりましたので HOGEHOGE_XCODE_VERSION_MINOR を使用して Xcode のバージョンを確認するようにコードを修正しましょう。

#if defined(HOGEHOGE_XCODE_VERSION_MINOR) && 0x0900 <= HOGEHOGE_XCODE_VERSION_MINOR
#error topLayoutGuide/bottomLayoutGuide is deprecated in iOS11. use safeArea in iOS11 if need and remove this condition.
#endif
self.tableView.contentInset = UIEdgeInsetsMake(self.topLayoutGuide.length, 0, self.bottomLayoutGuide.length, 0);
self.tableView.scrollIndicatorInsets = self.tableView.contentInset;

このコードを Xcode9.x 環境でビルドすると topLayoutGuide/bottomLayoutGuide is deprecated in iOS11. use safeArea in iOS11 if need and remove this condition. とエラーメッセージを吐きビルドが失敗します。つまり既存開発メンバは Xcode8.3.3 で開発しているためビルドは通るのですが、そのブランチを iPhoneX 対応のブランチにマージした際にビルドが失敗するようになるのです!!ビルドが失敗するのであとは iPhoneX 対応メンバが責任を持って修正し対応すれば良いことになります。

FAQ

Q. ビルド失敗させなくてもマージの際に修正すればいいのでは?

A. マージの規模が小さければ大丈夫かもしれませんが、大きい場合にはミスが発生する可能性が高くなります。コードの大部分は iPhoneX 対応のための追加の処理が必要ないのです。その箇所を生成されたマージコミットから漏れなく探すのが難しく、タイミングによっては複数の巨大な修正を一度に取り込む必要がありコントロールし辛いものであるため、その箇所をビルド失敗させることで漏れなく対応しました。

Q. ビルド失敗が発生するけど、直し方はどうやるの?

A. iPhoneX 対応メンバがレビュー時に Xcode9.x ビルドを失敗させる修正をさせたのと同時に対応方法も #error directive のメッセージで伝えたり必要であればコードに修正方法をコメントすることにした。そのためそれに従えばだれでもビルド失敗は治せる状態にしていました。

Q. 別にビルド失敗させなくても特定の識別子(例えば #TODO など)使えばいいのでは?

A. #TODO だらけのプロジェクトあったりしますよね...

Q. そもそもある一箇所でバージョンチェックするような修正で iPhoneX 対応できなくないですか?

A. そのためにあらかじめ Xcode8.x であるべき状態に WWDC 2017 のビデオ見て変更点を予想して対応しておきました。そうすることで Xcode9.x 対応は最小限ですみました。

Q. この手法って CI 環境とかで Xcode のバージョン追従漏れに気づいたりもできる?

A. はい。 iPhoneX 対応だけでなく Xcode のバージョンチェックに使うこともできます。詳しくは http://blog.dealforest.net/2014/11/プロジェクト毎に-xcode-のバージョンを指定/ を確認してください。

カヤックではエンジニアを大募集しています

カヤックでは普通の iOS エンジニアだけでなく、 WWDC のビデオを見るだけで次の iPhone を予想しそれに対応できるようなエスパーなエンジニアを募集しております。

明日は

アドベントカレンダー6日目を担当してくださるのは、きみーさんです。マットでマックスな人です。

*1:ちょっとしたハックなのですが XCODE_VERSION_MINOR は Xcode のバージョンで Xcode9.1 を使用した場合は 0910 という値となっています。そのため HOGEHOGE_XCODE_VERSION_MINOR=$(XCODE_VERSION_MINOR) と 0x を付与しない場合は 0 から始まる数値のため 8 進数と解釈され 8 進数で 9 はダメよと怒られてしまいます。そのため 0x を付与して 16 進数として扱うようにしています。