技術部モバイル基盤グループの ヴァンサンです。
西山が 以前紹介したように、クックパッドでは 2014 年から Swift を使っています。長い間、海外向けのアプリや みんなのお弁当だけに使われていましたが、去年の5月から、 クックパッド iOS アプリの開発にも Swift を使うようになりました。歴史のある iOS アプリなので Objective-C でのコードの方がまだ多いのですが、いまは既存の画面の変更を除いて新しいコードが Swift で書かれています。既存の画面を Swift で書き直すこともあります。
Xcode 8.0 がリリースされてから数ヶ月 Swift 2 を使っていましたが、去年の12月のリリース直後に Swift 3 へのマイグレーションをしてから、開発で Swift 3 を使っています。2017年2月1日にリリースされた 17.1.1.0 が Swift 3 を使ってビルドされた最初のバージョンです。
Swift 3 で変わったことを紹介するブログ記事は他に多くあるので、この記事では主にマイグレーションに焦点をあてて書こうと思います。
Swift 3 へのマイグレーションでは、殆どの Swift で書かれたコードが変わります。ですので、同じアプリでマイグレーションと並行して別の開発をすることは難しいです。そのため、年末年始辺りに開発のペースが落ちているのを利用して、2日間、他の開発を中断してマイグレーションを行いました。もちろん必要な期間はアプリのコードやその複雑さによりますし、早めに終わらせるために事前準備が必要ですね。
参考として、クックパッドiOSアプリのマイグレーションを行った時点では、ライブラリを含まなければ、 Swift のコードはファイル数264個(テストを含まなければ213個)、行数2.1万行(空行除けば1.7万)くらいでした。
事前準備
まず、アプリに使われる Swift でのフレームワークが Swift 3 に対応している必要があります。クックパッド iOS アプリでそれが問題になる可能性を見越していたのもあって、 Swift でのフレームワークの導入を抑えていました。マイグレーションの時点で社外 Swift フレームワークは Himotokiと Resultの2つだけだったので、社外フレームワークの Swift 3 対応に関して問題は特になかったです。
すべてのフレームワークが Swift 3 に対応していたら、試しに手元でマイグレーションをやってみることを強く推奨します。ビルド完成までいかなくても、何時間かやってみれば、色々見られると思います。特にマイグレーション中他の開発が進まないので、本番はできるだけスムーズに進みたいですよね。
僕が試しにマイグレーションをやって分かったことをいくつか紹介しましょう。
- マイグレーションツールが全部やってくれると思わない方がいいですね。マイグレーションツールを掛けた後に修正がかなり必要になるでしょう。
- マイグレーションツールが変な変更をすることがあります。例えば、クックパッドアプリでは
setTitle(buttonTitle, forState: .Normal)
がなぜかツールにsetTitle(buttonTitle, forState: UIControlState())
に変換されていました。そういう時、いつでもマイグレーション前のコードと比較できる状態でいる必要があります。あと、それをメモしておいて、次回ツールを掛けた直後にプロジェクトのファイルにそれを検索してすぐ直した方が早いですね。 #if
/#else
/#endif
の間に挟まれているコードはマイグレーションツールに無視されて、 Swift 2 のままです。なので、マイグレーションツールを掛ける前にできるだけ#if
/#else
/#endif
をコメントアウトして、ツールを掛け終わったら戻しておきましょう。- Carthage とかで入れたフレームワークはマイグレーションがアプリのコードと同時に行われたわけではないので、ツールが何が変わったのか把握しきれません。なので、フレームワークの変更の一部を自分でコードに適用する必要があります。試しにやったマイグレーションでどういう変更が必要そうなのかメモして、本番当日に一部を「全て置換」とかで変えられるようになった方がいいかもしれません。
また、マイグレーションの間に他の開発者が読める Swift 3 の仕様変更のドキュメントを用意してもいいですね。
マイグレーション本番
マイグレーション済みのコードがマージされるまで同じアプリで他の開発を中断しているので、本番は早めに終わらせる必要がありますね。本番の流れは基本的に以下の通りです。
- 上記に書いた通り、
#if
はできるだけコメントアウトします。 - マイグレーションツールを掛けます: Xcode で Edit → Convert → To Current Swift Syntax
- 事前準備でメモしたものを利用しながらビルドできるまでコンパイラーが出しているエラーを修正します。
事前準備でやるべきことをだいたい分かったはずなので、早いペースで進められるはずです。
ビルドできて、テストが通って、アプリを実行してみても問題なさそうな状態にもっていくのが重要です。少し不自然なコードは軽く修正できるならすぐ直してもいいのですが、それ以外の修正やもっと Swift 3 っぽくするのも後日でいいはずです。他の開発者が開発をできるだけ早く再開できてほしいですからね。
コードレビューされてからマージするのですが、変更が多いし、開発者がまだ Swift 3 に慣れていないのもあるでしょうし、しっかりしたコードレビューは難しいですね。あとで不具合があってもリリースまで気づく可能性を高めるため、マイグレーションを新しいバージョンの開発の開始時点で行いました。
マイグレーション後
折角 Swift 3 にしたので、 Swift 3 っぽくしたくなりますね。弊社では少し前から Swift 3 の API Design Guidelinesをある程度使っていたので、既存の Swift コードが割りと Swift 3 っぽかったです。とはいえ、できることがいくつかありました。
enum
Swift 3 では、 enum
の頭文字が小文字になっていて、マイグレーションツールは自動的に多くのを変えてくれるのですが、 String
の rawValue
を持つ enum
(enum MyEnum: String
で定義されたもの)は変えてくれません。値名を変えると値変わりますからね(実際値が明確に指定されているとしてもツールが変えてくれませんが)。そういう enum
を1つずつ確認して、変えても挙動が変わらなければ頭文字を小文字にした方がいいと思います。
クラスプロパティ
Swift 3 に伴って、 Objective-C でクラスプロパティを使えるようになりましたね。使うと Swift で余計な ()
が必要じゃなくなります。例えば
// 変更前 NS_ASSUME_NONNULL_BEGIN @interface CKDActivityLogger : NSObject + (instancetype)defaultLogger; // (省略)@end NS_ASSUME_NONNULL_END // 変更後 NS_ASSUME_NONNULL_BEGIN @interface CKDActivityLogger : NSObject // クラスプロパティは instancetype を使えないので、型は明確に@property (nonatomic, class, readonly) CKDActivityLogger *defaultLogger; // (省略)@end NS_ASSUME_NONNULL_END
それに合わせて Swift 3 側で CKDActivityLogger.default()
を CKDActivityLogger.default
変える必要があります。そういうところで ()
がない方が Swift っぽくて読みやすいと思います。
新しい Swift Foundation クラスのブリッジング
Swift 3 では、 Swift から見る Objective-C メソッドの引数の一部の型が変わりました。 NSDate
が Date
に、 NSURL
が URL
に、 NSIndexPath
が IndexPath
に、 NSError
が Error
に、など。
Objective-C 用のクラスに生やしていたヘルパーメソッドは一時的に as
で元の型にキャストしないと使えません。一番ややこしいのは Error
です。 Error
には、 NSError
と違って code
、 domain
、 userInfo
というプロパティがないので、 as NSError
がけっこう必要になります。
マイグレーション後、 Swift のコードに Objective-C Foundation の型(NSDate
, NSURL
, NSError
, …)が残ることありますが、 Swift でできるだけ Swift Foundation の型(Date
, URL
, Error
, …)に変えた方がコードがもっと Swift っぽくて読みやすい気がします。 Objective-C 用のクラスに生やしていたヘルパーメソッドを Swift 用の型にも生やした方が良いと思いますね(Objective-C を使わない場合、 Objective-C用のクラスにまだ生やす必要もないですね)。
Any
Swift 2 で AnyObject
になっていたところの多くが Swift 3 では Any
になります。マイグレーション後、それに合わせてアプリ内(ディクショナリの値とかで)残っている一部の AnyObject
を Any
に変えた方が、多くの as AnyObject
が消せてコードがもっとシンプルになる場合が多い気がします(実はマイグレーションツールも多くの as AnyObject
を入れたりします)。ただし、 Any
は AnyObject
と違って別の型にキャストしないとメソッドを呼べないので要注意です。
マイグレーション後に気づいた問題
現時点で Swift 3 にした影響で起きた問題は iOS 8 でしか起きない1つの問題だけです。
具体的に、 UITableViewDelegate
の tableView(_:heightForRowAt:)
で起きる問題です。 iOS 8 では、テーブルビューを使う画面から別の画面に遷移する時、 tableView(_:heightForRowAt:)
が呼ばれて、それに渡されている IndexPath
の row
が間違っています。4行あるテーブルの場合、row
が 0, 1, 2, 3 ではなく、 0, 0, 1, 2 になってしまいます。
もう少し調べてみると、 iOS 8 では、遷移の時だけ、渡された index path のクラスが UIMutableIndexPath
(NSIndexPath
のサブクラス)になっています。 Swift のソースを見たら分かりますが、 NSIndexPath
を Swift 用の IndexPath
への変換に getIndexes:range:
というメソッドが使われています。 iOS 8 では -[UIMutableIndexPath getIndexes:range:]
で取得される値が間違っているため、 IndexPath
に変換されて間違っている値になります。因みに、 -[UIMutableIndexPath indexAtPosition:]
は正しい値を返しているようなので、上記にリンクした IndexPath
の init(nsIndexPath:)
を以下のコードに変えたら動きそうですね(スピードとかに影響あるか分かりませんが)。
fileprivate init(nsIndexPath:ReferenceType) { letcount= nsIndexPath.length if count ==0 { _indexes = [] } else { _indexes = (0..<count).map { nsIndexPath.index(atPosition:$0) } } }
結局その問題が起きたビューコントローラは同じセクションでは高さ2種類だけだったので、2つのセクションを分けて回避できました。でもそういったバグが他にないか心配なので iOS 8 対応をまだしているアプリは Swift 3 が推奨しづらいですね。
最後に
クックパッド iOS アプリで Swift 3 のマイグレーションはしっかり準備した結果 iOS 8 での問題を除いて大きな問題は無かったです。 Swift 3 は変更が多いのですが、言語は色々改善されていると思いますので、 Swift 3 のマイグレーションをしてよかったと思っています。
そもそも近いうちに出る Xcode 8.3 はもう Swift 2.3 に対応しないので、 Swift で iOS の新しい機能を使いたかったら Swift 3 にしない選択肢はないですね。 Xcode 8 の Swift 2 対応にコードエディター関連問題がいくつかありますし。
最後に、 iOS 8 対応をしていない Swift アプリは早めに Swift 3 へのマイグレーションをした方がいい気がします。 iOS 8 対応をしているアプリは少し悩ましいですね。 iOS 8 対応を切るまで Xcode 8.2.1 で我慢するか、 Swift 3 にするけど iOS 8 での動作確認がしっかりするか、ですかね。後者は一部のコードを Objective-C で書く必要が出てくるかもしれませんが。
クックパッドでは Swift 3 で開発したいモバイルエンジニアを募集しています。