こんにちは、ヤフーでiOSアプリを開発している林です。
私が関わっているYahoo!ショッピングでは、iOSアプリをObjective-CとSwiftの混在状態で開発しています。今年の6月末からこのスタイルに切り替え、新規で書くコードは原則Swiftを使い、徐々にObjective-Cで書かれたコードを減らしている状況です。一方で完全にObjective-Cのコードを捨てることは現実的でないとも考えており、混在状態がこの先もしばらく続く想定でいます。
Yahoo! JAPANのアドベントカレンダー14日目は、この形に至った経緯・開発の進め方・そこから得られた知見を共有したいと思います。
プロジェクトが動き出すまでの経緯
Yahoo!ショッピングはサービスの歴史が長く、iOSアプリ版も2011年6月にVersion 1.0.0が審査提出されています。最も古いコードは2010年に書かれたものでした。
今年に入ってからサービス内でのアプリの存在感が強くなり、それに伴い案件の数が増え、メンテナンスコストが重くのしかかってくるようになりました。当初はしばらくの間Objective-Cでメンテナンスする予定でした。理由としては、Swiftがまだ発展途上であること、長く運営されてきた巨大サービスのiOSアプリを少しずつ置き換えていく前例はあまり見聞きしていない、どちらかというとコストへのネガティブな意見のほうが多かったことなどが挙げられます。
それでも、エンジニア間で作り直したいという意見が増えてきたり、Swiftで読み書きしやすく堅牢なコードを実現しようと積極的に働きかけるエンジニアが現れたりしていき、徐々に話が具体化して6月末から正式に動き出すことになりました。
既に大きなサービスであることによる制約
ただし、サービスの性質上、通常の機能改修を何ヶ月も止めてフルリニューアルを行うことは許されませんでした。新機能追加や日々入るキャンペーン企画の実装と平行して、コードを置き換えて行く必要がありました。また、長年積み重なった複雑な仕様全てにいきなり対応するということは不可能でした。そのため、全て一気に置き換えるという選択肢は現実的に難しく、数週間〜1ヶ月ごとに機能単位の置き換えを行っていくことにしました。
技術的な目標
進める上で、Objective-Cで書かれたコードをただSwiftの文法に置き換えるのはあまりやりたくありませんでした。それでは、Objective-Cで書かれたレガシーなコードをSwiftの文法で書かれたレガシーなコードへ置き換えるだけになってしまいます。メンテナンス性がより高く、どんどん増えていく変更要求に対して強いコードを実現するために設計から見直すことにしました。
当時、Swiftを使うことによって以下のメリットが得られるのではないかと考えていました。
- 強力な型システムによって、実行時に初めてわかる不具合を減らせる
- 記述量を圧倒的に減らすことができる
これらのメリットを最大限発揮するためには、Cocoaフレームワークの動的な性質を利用せざるを得ないViewやViewControllerだけではなく、Modelレイヤからしっかりと作り直していく必要がありそうでした。既に報告されている下記の先行事例等も参考にさせていただきつつ、自分たちのサービスに当てはめるとしたらどのような形が最適か検討していきました。
実際にやってきたこと・やってみてわかったこと
続いて、実際にチームでどのようにSwift移行を進めていき、どんな開発のルール作りがされてきたか、また、その中で得られた知見などを書いていきたいと思います。
Swiftへ置き換える際の方針
前述の背景を踏まえ、Yahoo!ショッピングでは
- 機能単位で区切った上で、MVCの全領域をまとめてリニューアルしていく
- 頻繁に変更が発生する機能を中心に置き換えていく
- Objective-Cのコードと共存しながらゆるやかに置き換えていく
- 新規に書く場合はまずSwiftで書けないか検討し、どうしても難しい場合のみObjective-Cを使う
という方針を取ることにしました。
APIクライアントやパーサーなどアプリ全体で利用される共通部分をまずSwiftで作り直し、それを使って各機能・画面を作成していました。GenericsやEnumを使ったパターンマッチングなどのSwiftが持つモダンな機能をできるだけ活用し、型安全だけれど柔軟なコードを実現できるよう試行錯誤しました。その際にはこちらのサンプルコードや、APIKitやSwiftyJSONなどの、Swiftの特徴を生かしているOSSの設計を参考にさせていただきました。
置き換えの流れ
例えば、Yahoo!ショッピングには商品をキーワードやカテゴリーで検索する機能があります。この機能をSwiftへ移行する時は、商品検索APIをたたくところから検索結果を表示させるところまでを1単位としてSwiftで新しく作りました。Objective-Cで書かれた他のViewControllerの検索窓をタップするとSwiftで書かれた検索結果機能へ遷移し、その中で使われるコードはModel-View-ViewControllerの全てがSwiftで作られています。そこから商品セルをタップするとObjective-Cで書かれた商品詳細情報のViewControllerへ遷移するというように置き換えました。
コーディング規約の作成
私たちの開発チームでは、必ずピアレビューによるコードレビュー後にブランチのマージ作業を行っています。Swiftのコードレビューを行う時の指針として利用できるように、コーディング規約も用意しました。開発者の数が多く、ほとんどははじめてSwiftを触るという状況で、あらかじめどのように書くのか明文化しておく必要が生じたためです。また、人によってスタイルが変わりやすい微妙な部分に関して、できるだけ合意をとっておきたいという思惑もありました。たとえば、
- 型推論などによってさまざまなところで省略が可能だが、その分可読性が下がってしまう。
- エラーハンドリングはSwiftに組み込まれているthrowsで行うか、コミュニティ内でよく推奨されているResultやEitherを使うか
といったことは、実際にみんなで時間をとって話し合いながらどのようにしていくかを決めていきました。
既にウェブ上には良質なコーディング規約が数多く公開されており、Yahoo!ショッピングはWantedlyさんのガイドラインを参考にさせていただきながら作成しています。
今であればAppleから公式にSwiftのAPI Design Guideが提供されているため、こちらも参考になるでしょう。
「!」は原則禁止に
SwiftのOptional型の説明は既にさまざまなところでされているため、ここでは割愛させていただきます。
Yahoo!ショッピングではコーディング規約にて、Forced Unwrapping, Implicit Unwrapped Optionalを原則禁止にしています。ただし全く使っていないわけではなく、利用可能ケースは以下に限定すると明確にしています。
- IBOutletの接続
- storyboard, xibからのイニシャライズ
- UITableView, UICollectionViewからCellをdequeueする時
もしこれ以外で例外的に使う場合は必ず、「!」を使ってかまわない・いっそのことアプリを落としてしまいたい理由をコメント文で説明するという縛りを設けています。これは過剰な制約のように感じられる方もいらっしゃるかもしれません。しかし複数人で長期間プロジェクトを進めていく上では「!」の存在がなかなか厄介で、今はこの形に落ち着きました。
- 副作用を持つ処理では、実装者が想定していなかった型のデータが来ることが十分にありうる
- 時間がたつと、どれは書かれた当時に想定したとおり動き、どれは問題が発生しそうか簡単には見極めにくい
これらは個人で小規模なプロダクトを扱う場合であれば全体が把握できているため問題とならないでしょう。また、熟練したエンジニアであれば使い所を任せてしまってもいいかもしれません。スキルがさまざまなメンバーによるチーム開発の場合、ここを個人の裁量に任せてしまうとケアレスミスによるクラッシュの温床になります。大規模なプロダクトを複数人で扱う場合には、「!」はその一文字にコンテキストを含みすぎていて、開発者の注意力と洞察力に依存する部分が増えてしまいます。「!」に対しては明確なルールを作るのがいいと思っています。
バージョンアップ対応
iOSでは、OSがバージョンアップされるタイミングでその対応のためのコード修正を行うのが恒例行事になっているかと思います。これはSwiftでも同じことがいえます。Swiftに置き換える決断をしたタイミングではバージョンはまだ1.2でした。その後、WWDC 2015でSwift 2.0が発表され、do-try-catchやguardなどの新機能と共に、一部のグローバル関数の廃止や標準ライブラリの引数の変更などがありました。
その結果、Swift 2.0に対応したXcodeでそれまでのコードをビルドすると全然コンパイルが通らないという状況になりました。
おそらくこの後方互換性を切ったドラスティックな変化が理由で、Swiftの利用に踏み出せない方も多いかと思います。実際に経験してみた感想としては、「対応はそれほど大変ではなかった」といったところでした。基本的には機械的な置き換えで済む部分が多く、型をしっかり意識した作りにしていたため、Xcodeの一括置換の機能を使いながらビルドが通るように修正していくだけで、Xcode7のGM版がでて1週間もたたずに移行が完了しました。
この先Swift 3.0が待ち構えていますが、今回のバージョンアップも後方互換性はないようです。ロードマップがGitHub上に公開されているので、事前に心とコードの準備をしておくとよいかと思います。
これを見ると、例えば++/—の廃止などが既に決定されています。できる限り++/—は使わず+= 1などを使うように心がけたほうが後々楽でしょう。
Swiftにこだわらず柔軟にObjective-Cも使用
Swiftの標準ライブラリは十分にそろっているとは言いがたい状況です。例えばちょっとした文字列処理でもFoundationの資産を使う必要がありますし、Webサービスのフロントとしてアプリを開発している場合、主な実装はUIKitを使うことになります。ヤフーの場合、アプリ間で使いまわされている社内ライブラリが数多く存在しますが、それらもまだ大部分はObjective-Cで書かれています。SDWebImageなどのObjective-C製の良質なサードパーティライブラリも手放すことはできません。実際にプロジェクトを進めてみると、思ったよりSwift製のものだけでなんとかなる部分は少なかったです。あまりSwiftにこだわりすぎず、できる範囲から置き換えていくと割りきったほうが気持ちの上でも楽ですし、実際のSwiftを取り巻く状況に即していると思います。
Objective-CとSwiftで相互参照するときに発生した問題
SwiftからObjective-Cを参照する場合はBridging Headerを用意して、そこに必要なクラスをimportするだけで済みます。呼び出す際の大きな制約もありません。
ただし、気をつける必要があるのは、現状(Swift 2.1)では、Nullable, NonNullの指定がされていないObjective-CのコードがImplicit Unwrapped Optionalになってしまう点です。これを受けてチームのガイドラインで、サードパーティ製のObjective-Cライブラリのメソッドを呼び出す場合などではOptional Chainingを行うという決まりを作っています。
Objective-CのクラスをSwiftで利用する場合の詳細は、以下のドキュメントに載っています。
Objective-CからSwiftを呼ぶ場合はビルド時に自動生成される.hファイルをimportするだけです。呼び出したいObjective-Cのファイル内で、以下のimport文を書くだけです。
#import "[Targetのモジュール名]-Swift.h"
Objective-CからSwiftのクラスを利用する場合には多くの制約があります。Genericsや(Int型のraw valueを持たない)Enumなど、Swiftを特徴づける言語機能が利用できません。詳細は以下のドキュメントを参照して下さい。
チーム内では、Objective-CからSwiftのコードを呼ぶ時にはUIViewやUIViewControllerのレイヤのものを呼び出すようにして、極力ModelはObjective-Cからの利用を意識せずに作れるようにする、というガイドラインにしています。
相互参照についてはAppleの公式ドキュメントに詳しく載っているのでこちらをご参照下さい。
ユニットテストのやり方がObjective-C時代と少し変わる
Yahoo!ショッピングのiOSアプリでは、Modelレイヤへ変更を加える場合にはユニットテストを記述するというガイドラインを敷いています。ただしSwiftの型システムを有効活用することで、ビルドのタイミングである程度のバグをつぶすことができ、Objective-Cに比べたら必要なユニットテストの量が減っています。
モック・スタブに関しては、Manual Mockingという方法を使っています。Swiftはリフレクションの機能があまり充実していないため、実行時にメソッドの中身を書き換えるといった黒魔術が利用できません(NSObjectを継承している場合を除く)。その代わりとして、Manual Mockingを利用することにしました。テスト用の関数内で、スタブ化したいクラスのサブクラスを作成し、メソッドをオーバライドします。こうすることでモッククラスのスコープをその関数内だけにすることができて便利です。
例えば以下のようなクラスをテストしたいとします。
class DataSourceFetcher {
var loginManager = LoginManager.sharedInstance
func execute(completeHandler handler: (data: [String], error: NSError) -> Void) {
if loginManager.isLogin() {
...
} else {
...
}
}
}
テストケースの中でテストしたいクラスの依存クラスに対するモックを用意します。それをテストしたいクラスに対して注入し、テストを実行します。
class DataSourceFetcherTests: XCTestCase {
...
...
func testFetchWhenLogin() {
class LoginManagerMock: LoginManager {
override func isLogin() -> Bool {
return true
}
}
let exception = expectationWithDescription("通信失敗")
let fetcher = DataSourceFetcher()
fetcher.loginManager = LoginManagerMock()
fetcher.execute { [weak self] (data, error) in
XCTAssertNotNil(data, "結果データの取得")
self?.exception.fulfill()
}
waitForExpectationsWithTimeout(10.0) { (error) in
if let _ = error {
XCTFail("リクエストの結果を受け取れませんでした。")
}
OHHTTPStubs.removeAllStubs()
}
}
}
これと通信周りのスタブを作成できるOHHTTPStubsの組み合わせで、今のところは問題なくユニットテストを書くことができています。
iOSプログラミング入門者にとってのとっつきやすさ
Yahoo!ショッピングではiOSアプリのプログラマになってまだ日が浅いエンジニアも多くいます。彼・彼女ら見ていると、SwiftよりもObjective-Cのほうがハマりどころが多いように感じました。例えばnilをメソッドの引数やDictionaryの要素として扱ってしまった時のクラッシュは皆様なじみ深い(?)かも知れません。そういったものも、SwiftであればOptionalによってnilが入ってくる可能性のハンドリングを強制されるため、作る側もレビューする側も問題に気が付きやすいです。
さらに、記述量の少なさやスクリプト言語に近い文法などを考えると、入門者にとってはSwiftのほうがはるかにとっつきやすいと思います。
ビルドが遅いというデメリット
ウェブ上の各所で言われている通り、Swiftで書かれたコードはコンパイルにかなり時間がかかります。Swift 1.2から導入されたIncremental buildsの力で、開発中のビルドはかなり早くなりました。しかし開発ブランチを切り替えたり、アドホックパッケージを作成する時などで全体のコンパイルをやり直すと、相変わらずビルドに時間を取ります。これで集中力を奪われてしまうのがなかなかしんどく、Swiftにしたことで逆に効率が悪くなった点だと思います。
Swiftに置き換えてよかったか
Swiftのよい点・悪い点も含めていろいろ書かせていただきましたが、結果的にはSwiftに移行を決めてよかったと思っています。型のおかげで仕様変更や機能追加に対して強いコードが書けるようになりました。また、クラスの設計の仕方次第では、Objective-Cと共存しつつもSwiftの言語機能を最大限活用したコードを書くことが可能です。Objective-CとSwiftのクラスを混在させることも比較的簡単にでき、コストはほとんどありません。難点を挙げるとすれば、エンジニアの新しい言語に対する学習コストでしょう。特にこれまで長年ずっとObjective-Cをやってきたエンジニアにとっては脳のスイッチングコストが大きいかもしれません。
本記事では、Yahoo!ショッピング内で実際にSwiftへの置き換えプロジェクトを実行してみて、そこから得られた経験を書かせていただきました。皆様のSwift移行の検討材料としてお役に立てることを願います。
こちらの記事のご感想を聞かせください。
- 学びがある
- わかりやすい
- 新しい視点
ご感想ありがとうございました