iOSエンジニアの高(@y_koh)です。
この度トレタではiPadアプリのSwift 3対応を行いました。どんな感じで進めたのかと、ハマったところなど共有できればなと思います。
対応自体は去年末には終えていましたが、年が明けて1/10にリリースしました。 年末は飲食店さま繁忙期のため、トレタではこの時期のアプリアップデートは控えています。例年このタイミングでリファクタリングやKaizenタスクなどを行っています。今回はSwift 3対応をメインに行いました。
先日サーバサイドもRailsを4.2にバージョンアップしています。言語やフレームワークのバージョンアップは機能改善に直接つながるものではないので後回しにしがちですが、将来的に負債になってしまうだけなので出来る限り時間を作って適宜アップデートするようにしています。
今回のSwift 3対応については、昨年のpotatotips#35でも発表させていただきました。 speakerdeck.com
概要
コード行数
9万行ほど
Swift使用率
トレタのベースはObjective-Cです。Swiftの占める割合は全体の2割ほどですが、コアなUI部分からSwift化しているので割りと複雑なところが多いです。基本的なスタンスとして新規追加もしくはリファクタリングを行うタイミングでSwiftを使用するようにしています。
その結果APIやモデル周りがまだObjective-Cのままなので、今後はこちらを設計見直しと合わせて対応していければと思っています。
期間
2〜3週間ほどかかりました。
進め方
こんな流れで進めていきました。
- 作業ブランチを切る
- ライブラリをアップデートする
- XcodeでSwift 3にConvertする
- コンパイルエラーをひたすら潰す
- 動作確認
作業ブランチを切る
iPadアプリはエンジニア3名体制で開発しているので他の開発と並行して作業することになります。feature/swift3
ブランチで作業して、developに加わった変更を随時取り込んでいくことになります。
当然ながらdevelopから離れれば離れるほど大変になるので、迅速な対応が求められます。
ライブラリをアップデートする
ライブラリ自体がSwift 3対応している必要があるので対応状況を調査するところから始めます。 READMEにしっかり書いてあるものもあれば、何も書かれておらずブランチのみ用意されているものもあります。最悪メンテされていない場合は他のライブラリに乗り換える必要も出てきます。
時期的にもほとんどのライブラリは対応されていると思います。もし現状対応されていなければ乗り換えを検討されたほうが良いかもしれません。
またSwiftライブラリだけでなく、Objective-Cライブラリもnullabilityアノテーションに対応されているケースもあるので確認してみると良いと思います。
XcodeでSwift 3にConvertする
convertしようとするとこんな感じアラートが出て来てconvert出来ずにしばらく悩みました。
http://stackoverflow.com/questions/39492974/tests-stop-working-under-xcode-8-test-host-error
こちらにあるように、テストターゲットのTesting > Target Application
をNone
にすることで対応できました。
コンパイルエラーをひたすら潰す
ここからはコンパイルエラーをひたすら潰していきます。エラー数が多いとコンパイラが途中で諦めてしまうため、エラー数の総量を知ることが出来ません。よって、エラーを解消して残エラー数が減っていくとあるところでガッツリ増える、ということを繰り返してくことになります。
これに関してはどうすることも出来ないので諦めない心で頑張るしか無いです。コツとしては、まず全体を知るために、エラー解消が難しいところはちゃんとメモに残した上で、コメントアウトしてでも一旦通すのが良いと思います。
主要ライブラリ対応
SwiftBond
Migration from v4.x to v5.0を参考にMigrationします。
observeNew
がしれっと無くなっているのでskip
で代用します。
hoge.skip(first: 1).observeNext
また、bnd_notification
の場合は定義時にはクロージャは走らなくなっているのでskip
する必要はありません。これに気づかずなぜか一回目だけ挙動がおかしいというのでハマりました。。
ライブラリ以外での変更
DispatchOnce
Swift 3 からはdispatch_once
に相当するものが提供されなくなりました。
こちらはlazy initialized globalsもしくはstaticプロパティで代用できます。
こちらの記事に詳しくまとめられていたので参照してみると良いと思います。 Swift3のGCD周りのまとめ - Qiita
Date
Swift 3からNSDateのSiwft実装Dateが提供されるようになりました。これにより、MTDatesというObjective-Cライブラリを使用していたのですが、NSDateのカテゴリ実装のためDateでは使えません。よってこの様にキャストする必要があります。
この時はまずビルドを通すことを目標にしていたので、地道に書き換えてましたが、後にブリッジするためのDate Extensionを作ってます。
(reservation.startAt as NSDate).tr_startOfToday())
CGFloat の剰余
これは少し驚いた変更でした。
y % heightByHour ↓ y.truncatingRemainder(dividingBy: heightByHour)
https://developer.apple.com/reference/swift/floatingpoint/1847167-truncatingremainder
Int型の場合はいままでどおり%
が使えます。
@escaping
Swift 3 の @escaping とは何か - Qiita
@noescapeがdeprecatedになり、逆に@escapingが必須になりました。
@escapingの有り無しで別メソッド扱いになるみたいで、interfaceに@escapingを付け忘れたときに呼べないということがありました。
removeRange
エラー内容がわかりにくくてちょっと悩みました。
courseLabels.removeRange(CountableRange<Int>(courses.count ..< courseLabels.count)) ↓ courseLabels.removeSubrange(CountableRange<Int>(courses.count..<courseLabels.count))
iOS 10対応
ユーザーデータアクセス
写真選択時にエラーになりました。
This app has crashed because it attempted to access privacy-sensitive data without a usage description. The app's Info.plist must contain an NSPhotoLibraryUsageDescription key with a string value explaining to the user how the app uses this data.
iOS 10 からInfo.plist
にユーザーデータへアクセスする目的の記述が必須になっているのでこちらを対応しましょう。
[iOS 10] 各種ユーザーデータへアクセスする目的を記述することが必須になるようです | Developers.IO
その他
テストが動かない
Could not determine bundle identifier for xxxTest TEST_HOST: some path that does not exist
こんなエラーが出てきましたが、テストターゲットのHostApplicationを切り替えてみたら動きました。
IUOが無くなった影響で、文字列内式展開でOptionalが出力されてしまった
ラベルにそのまま"Optional(y_koh)"のように表示されてしまいました。これはUnwrapすることで対応しました。
var user = User() // modelはObjective-Cでnullabilityアノテーション指定なし var label = UILabel() label.name = user.name
↓
var user = User() // modelはObjective-Cでnullabilityアノテーション指定なし var label = UILabel() if let user = user { label.name = user.name }
Swift 3 対応時にハマったString Interpolate - Qiita
iOS 9でクラッシュ
Swift 3というかSDKの違いかもしれません。
let cell: TableLayoutListCell = tableView.dequeueReusableCell(withIdentifier: "ReservationCell", for: indexPath) as! TableLayoutListCell
上記を実行すると、
- iOS 9
layoutSubviews
が呼ばれる - iOS 10
layoutSubviews
が呼ばれない
となって挙動が変わっています。今回は先にlayoutSubviewsが呼ばれることを想定していないコードになっていたのでUnwrap時にnilで落ちていました。
まとめ
Swift 3対応はアプリの規模にもよりますが、かなり根気のいる作業となります。これから対応される方は最初から完璧にやろうとせずにまずはコメントアウトしながらでもビルドを通すことを目標にすると良いと思います。
少しでもこれからSwift 3対応される方の助けになれば幸いです。