こんにちは。
iOS版のAmebaアプリを開発している @tasanobu と @nghialv2607 です。
しかし、昨年10月 Swiftを開発言語として使う という決定をしました。
前述したフルネイティブ化していく大規模改修が昨年末からスタートすることが決定しており、アプリの基盤部分をSwiftへリプレースするにはベストなタイミングだと考えたためです。数年後のiOSアプリ開発の状況を想像すると、Swiftで実装することが主流になっているはずで、Objective-Cで実装し続けること自体が技術的負債を生む行為になると考えたからです。
とはいえ、Amebaアプリは弊社随一のユーザ規模を誇ります。そのアプリでSwiftを採用するのは大きな決断でしたが、Objective-CやCocoaのフレームワークとの互換性が担保されている点が決め手となりました。Amebaアプリのコードベースは巨大なため、数ヶ月程度では全てをSwiftで実装し直すことは不可能です。この互換性により、完全Swift化までの過渡期として、新しく追加するSwiftのコードとこれまでのObjective-Cで実装された資産を共存させることができるからです。
このくらいの人数がいると、どうしても担当者によって設計や実装がバラバラになりがちです。
メンバー間で認識を合わせて効率的に開発を進めるため、次の項目をルール化しました。
そこで下記のコーディング規約を採用し、原則としてこの規約に則ることにしました。
こういった場合、Objective-Cとの連携を意識してしまうと、Swiftにしかない言語機能をどうしても使いにくくなってしまいます。
そのため、原則としてObjective-Cとの連携は考慮せず設計・実装を行うようにしました。
Objective-Cとの互換性がないEnum、Struct、Genericsなどの言語機能を利用したことにより、既存のクラスと連携できないようなこともありましたが、その場合はむしろ積極的に既存のクラスをSwiftで書き直すようにしました。
幸いAmebaアプリでは、EnumとNSNotificaitonやNSUserDefaultsで使う文字列定数をSwift/Objective-C間で共有できれば連携できる形になっていました。
そのため、Objective-CとSwift間の定数ブリッジ用ヘッダを一つ用意し、このヘッダに言語間で共有する定数を集約することにしました。
この方法はSwiftとObjective-Cが混在する過渡期の暫定対応と位置付けております。
今後、既存クラスの書き換えが進み、そもそも定数を共有する必要がなくなれば、削除する予定です。
Webサービスのクライアントアプリによくある表示用データを取得する処理を例にして説明させて頂きます。
次のサンプルは表示用データをWeb APIから取得するメソッドです。
内部的にはHTTPレスポンスのJSONをパースし、モデルクラスへの変換まで行っておりますが、APIを呼び出す箇所ではとてもシンプルな記述にできます。また、API.Topic.getDailyRankings()のように.(ドット)で機能毎に区切られ、どういった機能のAPI呼び出しかを分かりやすくしています。
API構造体を`Class`にしなかったのは、Swift 1.1では言語仕様として`stored type property`が提供されておらず、`API.version`のような文字列定数を使うことができないからです。Swift 1.2から`Class`でも`stored type property`が利用できるので、そのうちClassに変更するかもしれません。
API.Topic.getDailyRanking()の内部ではAPI.fetchModelData()を呼び出しています。
モデルクラスでは引数に渡されるSwiftyJson型の値からモデルクラスを初期化します。Failable Initializer(※戻り値がnilになることもあるInitializerです。)にしているのは、引数に想定しない値が渡されることを考慮しているためです。
Amebaアプリでは各モデルクラスにisValidというcomputed propertyを実装しており、Failable Initializer内でisValidがfalseの場合は初期化失敗という意味でnilを返すようにしています。
モデルクラスの初期化はHTTPレスポンスとして返されるJSONのルートから直接行わず、所定のキーに対応する値を使いたいことがあります。この問題に対応するためにsubJsonクロージャを用意しました。
getDailyRanking()ではsJsonとして{ $0["data"] }を指定しており、Topic Dataの"data"キーの値を使って初期化するようにしています。
採用したSwiftのライブラリは次の通りです。
現在AmebaアプリではJSONのキャッシュ機能のみ利用しています。もともとは画像のキャッシュ機能も利用したかったのですが、導入時にはまだ画像キャッシュ機能が不安定だったため、利用を見送りました。
画像キャッシュにはObjective-CのSDWebImageというライブラリを利用しています。
UITableViewをUITableViewDelegateとUITableViewDataSourceプロトコルを実装することなく簡単に使うことができます。
UITableViewを使った画面では、その画面仕様の複雑さに比例してUIViewControllerが肥大化してしまいます。
このライブラリを使うと、UIViewController上でUITableViewDelegateとUITableViewDataSourceを実装する必要がなくなるため、肥大化問題をうまく解決できます!
そのため、各ライブラリはgit subtreeを使ってプロジェクトに追加しています。
通常、外部リポジトリを依存管理する場合はgit submoduleでいいと思います。
Amebaアプリの仕様上、一部のライブラリを修正する必要があったため、全ライブラリの取り込みにgit subtreeを使うことにしました。
こんなことをせずに、単純にCocoapodsで依存管理できる日が来ることを心待ちにしております。
Swiftに移行したことによる苦労(ビルド遅い、Swift Optimize Optionにより挙動の違い発生するなど)はありましたが、チームメンバーは総じてObjective-CからSwiftに移行したことに満足しています。
既存プロジェクトの場合、移行のタイミングを見定めるのは非常に難しいですが、Xcode 6.3ではSwift関連の改善が多く盛り込まれますので、ぜひ検討してみてはいかがでしょうか?
予想しているより、移行へのハードルは高くないと思いますよ!
長文・乱文の中、最後までお読み頂きありがとうございました。
今回のエントリが、Swiftへの移行に興味がある方の参考になれば幸いです。
iOS版のAmebaアプリを開発している @tasanobu と @nghialv2607 です。
これまでのAmebaアプリはWebViewとのハイブリッド形式でしたが、UI/UXを改善すべくフルネイティブ化して大幅にリニューアルしました。
数ヶ月間に及ぶアプリの構成を大幅に変えるプロジェクトは、Objective-CではなくSwiftで実装を進めました。
このエントリでは、Objective-CのコードベースをSwiftへ移行していくために行った取り組みを紹介させて頂きます。
このエントリの公開時点では、Swiftを既存プロジェクトに導入した話はインターネット上でみかけません。
今後、多くのプロジェクトでObjective-CからSwiftへ移行する作業が行われると思いますので、その際の参考にして頂けると幸いです。
Swift導入の経緯
Amebaアプリは2009年にストアにリリースされ、5年以上も運用されているプロジェクトです。コードベースは巨大で、その全てがObjective-Cで実装されていました。こういったケースでは「Swiftを使うなら新規プロジェクトで」という判断をしがちで、Objective-Cでの開発を継続することが多いのではないかと思います。しかし、昨年10月 Swiftを開発言語として使う という決定をしました。
前述したフルネイティブ化していく大規模改修が昨年末からスタートすることが決定しており、アプリの基盤部分をSwiftへリプレースするにはベストなタイミングだと考えたためです。数年後のiOSアプリ開発の状況を想像すると、Swiftで実装することが主流になっているはずで、Objective-Cで実装し続けること自体が技術的負債を生む行為になると考えたからです。
とはいえ、Amebaアプリは弊社随一のユーザ規模を誇ります。そのアプリでSwiftを採用するのは大きな決断でしたが、Objective-CやCocoaのフレームワークとの互換性が担保されている点が決め手となりました。Amebaアプリのコードベースは巨大なため、数ヶ月程度では全てをSwiftで実装し直すことは不可能です。この互換性により、完全Swift化までの過渡期として、新しく追加するSwiftのコードとこれまでのObjective-Cで実装された資産を共存させることができるからです。
開発ルール
AmebaアプリのiOSチームには常時3、4名のメンバーが所属しています。このくらいの人数がいると、どうしても担当者によって設計や実装がバラバラになりがちです。
メンバー間で認識を合わせて効率的に開発を進めるため、次の項目をルール化しました。
1. コード規約
Swiftは言語仕様として強力な型推論を持ち、型の宣言を省略することが可能です。また、Closureなどでは記述を大幅に簡素化して実装可能です。この辺りは人によって好みが出やすく、ルール化しないと統一感がなくなってしまいます。そこで下記のコーディング規約を採用し、原則としてこの規約に則ることにしました。
2. Swiftで実装する機能はObjective-Cとの連携を考慮しない
新しいクラスはSwiftで実装することになりますが、現在はSwiftへの移行の初期段階のため、連携するクラスは既存のObjective-Cで実装されていることが大半です。こういった場合、Objective-Cとの連携を意識してしまうと、Swiftにしかない言語機能をどうしても使いにくくなってしまいます。
そのため、原則としてObjective-Cとの連携は考慮せず設計・実装を行うようにしました。
Objective-Cとの互換性がないEnum、Struct、Genericsなどの言語機能を利用したことにより、既存のクラスと連携できないようなこともありましたが、その場合はむしろ積極的に既存のクラスをSwiftで書き直すようにしました。
3. 定数ブリッジ用ヘッダに言語間で共有する定数を集約する
とはいえ、Objective-Cの資産と連携せざる負えないことがあります。(よくないことなのですが、Objective-Cのクラスの依存関係が複雑すぎてサクッとSwiftに書き換えることができない 等々。長いこと運用している場合、ありますよね。こういうこと。)幸いAmebaアプリでは、EnumとNSNotificaitonやNSUserDefaultsで使う文字列定数をSwift/Objective-C間で共有できれば連携できる形になっていました。
そのため、Objective-CとSwift間の定数ブリッジ用ヘッダを一つ用意し、このヘッダに言語間で共有する定数を集約することにしました。
この方法はSwiftとObjective-Cが混在する過渡期の暫定対応と位置付けております。
今後、既存クラスの書き換えが進み、そもそも定数を共有する必要がなくなれば、削除する予定です。
Swiftの言語機能を利用したAPI設計
Objective-Cとの連携を意識しない方針により、各所でSwiftならではの設計を採用しました。Webサービスのクライアントアプリによくある表示用データを取得する処理を例にして説明させて頂きます。
次のサンプルは表示用データをWeb APIから取得するメソッドです。
内部的にはHTTPレスポンスのJSONをパースし、モデルクラスへの変換まで行っておりますが、APIを呼び出す箇所ではとてもシンプルな記述にできます。また、API.Topic.getDailyRankings()のように.(ドット)で機能毎に区切られ、どういった機能のAPI呼び出しかを分かりやすくしています。
API.Topic.getDailyRankings(limit, offset) { // ... }
API.Search.searchByName(keyword) { // ... }
Nested Classを使ったモデルクラスの宣言方法
API構造体に対して、各機能をextensionで分割し、`Nested Class`を使うことでName Space的な .(ドット) 区切りの見た目を実現しています。API構造体を`Class`にしなかったのは、Swift 1.1では言語仕様として`stored type property`が提供されておらず、`API.version`のような文字列定数を使うことができないからです。Swift 1.2から`Class`でも`stored type property`が利用できるので、そのうちClassに変更するかもしれません。
struct API {
static let version = "v1"
// 他の実装
}
extension API {
class Topic {
// 他の実装
}
}
Genericsを使ったモデル生成処理の共通化
API.Topic.getDailyRanking()の内部ではAPI.fetchModelData()を呼び出しています。protocol JsonInitializable { // failable initializer init?(sJson: SwiftyJson) }
struct API { static let version = "v1"
static func fetchModelData<T: JsonInitializable>(url: String, settings: APISetting, subJson: (SwiftyJson) -> SwiftyJson, completionHandler: (T?, NSError?) -> ()) { AMBAlamofire.requestApi(.GET, URLString: url, setting: settings) .responseApiSwiftyJson { _, _, sJson, error in if error != nil || sJson == nil { completionHandler(nil, error) return } if let top = subJson(sJson!) ?? sJson { completionHandler(T(sJson: top), error) return } completionHandler(nil, error) } } }
JsonInitializableプロトコル
JsonInitializableはモデルクラスが継承するプロトコルです。モデルクラスでは引数に渡されるSwiftyJson型の値からモデルクラスを初期化します。Failable Initializer(※戻り値がnilになることもあるInitializerです。)にしているのは、引数に想定しない値が渡されることを考慮しているためです。
Amebaアプリでは各モデルクラスにisValidというcomputed propertyを実装しており、Failable Initializer内でisValidがfalseの場合は初期化失敗という意味でnilを返すようにしています。
extension API { class TopicData : JsonInitializable { required init?(sJson: SwiftyJson) { title = sJson["title"].string if !isValid { return nil } }
var isValid: Bool { return title != nil } }
subJsonパラメータ
モデルクラスの初期化はHTTPレスポンスとして返されるJSONのルートから直接行わず、所定のキーに対応する値を使いたいことがあります。この問題に対応するためにsubJsonクロージャを用意しました。getDailyRanking()ではsJsonとして{ $0["data"] }を指定しており、Topic Dataの"data"キーの値を使って初期化するようにしています。
extension API { class Topic { typealias CompletionHandler = ([TopicData], NSError?) -> () class func getDailyRanking(limit: UInt, offset: UInt, completionHandler: CompletionHandler) { let urlString = TopicRouter.Ranking(.Yesterday, limit, offset, ageParam).URLString API.fetchModelData(urlString, setting: APISetting(), subJson: { $0["data"] }, completionHandler: completionHandler) } }
// Topic Data { "version": "v1", "data": { "title": "This is the topic title!" ... } }
以上がざっくりとしたAPI設計の内側です。
Swiftの言語機能を利用することでObjective-Cを使った場合と比較すると、かなりスッキリと実装できることがお分かりになるかと思います。
Swiftライブラリ
Objective-Cのコードを使いたくないという考えが根底にありますので、Swiftで実装されているライブラリを使うようにしました。採用したSwiftのライブラリは次の通りです。
Alamofire
AFNetworkingでおなじみのMattt Thompsonさんが開発しているネットワークライブラリです。Swiftの綺麗なシンタックスとメソッドチェインを利用してHTTP関連の処理をスッキリと書くことができます。SwiftyJson
Swiftは型に厳しいのでJSONのパース処理は非常に面倒くさいのですが、このライブラリを利用するとシンプルにJSONを扱えるようになります。HanekeSwift
画像とJSONのキャッシュライブラリです。現在AmebaアプリではJSONのキャッシュ機能のみ利用しています。もともとは画像のキャッシュ機能も利用したかったのですが、導入時にはまだ画像キャッシュ機能が不安定だったため、利用を見送りました。
画像キャッシュにはObjective-CのSDWebImageというライブラリを利用しています。
Hakuba
私@nghialvが開発しているライブラリです。UITableViewをUITableViewDelegateとUITableViewDataSourceプロトコルを実装することなく簡単に使うことができます。
UITableViewを使った画面では、その画面仕様の複雑さに比例してUIViewControllerが肥大化してしまいます。
このライブラリを使うと、UIViewController上でUITableViewDelegateとUITableViewDataSourceを実装する必要がなくなるため、肥大化問題をうまく解決できます!
PullToRefresh
@dekatotoroさんが開発しているPullToRefreshライブラリです。SlideMenuControllerSwift
@dekatotoroさんが開発しているスライドメニューライブラリです。ライブラリの利用方法
現在、AmebaアプリはiOS 7.0以上をサポートしており、SwiftのライブラリをDynamic Frameworkとして使うことができません。。。そのため、各ライブラリはgit subtreeを使ってプロジェクトに追加しています。
通常、外部リポジトリを依存管理する場合はgit submoduleでいいと思います。
Amebaアプリの仕様上、一部のライブラリを修正する必要があったため、全ライブラリの取り込みにgit subtreeを使うことにしました。
こんなことをせずに、単純にCocoapodsで依存管理できる日が来ることを心待ちにしております。
まとめ
AmebaアプリにおけるSwiftへ移行するための取り組みを紹介させて頂きました。Swiftに移行したことによる苦労(ビルド遅い、Swift Optimize Optionにより挙動の違い発生するなど)はありましたが、チームメンバーは総じてObjective-CからSwiftに移行したことに満足しています。
既存プロジェクトの場合、移行のタイミングを見定めるのは非常に難しいですが、Xcode 6.3ではSwift関連の改善が多く盛り込まれますので、ぜひ検討してみてはいかがでしょうか?
予想しているより、移行へのハードルは高くないと思いますよ!
長文・乱文の中、最後までお読み頂きありがとうございました。
今回のエントリが、Swiftへの移行に興味がある方の参考になれば幸いです。