こんにちは。iOS版のAmebaアプリを開発している @tasanobu @nghialv2607 です。

これまでのAmebaアプリはWebViewとのハイブリッド形式でしたが、UI/UXを改善すべくフルネイティブ化して大幅にリニューアルしました。

数ヶ月間に及ぶアプリの構成を大幅に変えるプロジェクトは、Objective-CではなくSwiftで実装を進めました。





このエントリでは、Objective-CのコードベースをSwiftへ移行していくために行った取り組みを紹介させて頂きます。

このエントリの公開時点では、Swiftを既存プロジェクトに導入した話はインターネット上でみかけません。

今後、多くのプロジェクトでObjective-CからSwiftへ移行する作業が行われると思いますので、その際の参考にして頂けると幸いです。





Swift導入の経緯

開発ルール

1. コード規約

2. Swiftで実装する機能はObjective-Cとの連携を考慮しない

3. 定数ブリッジ用ヘッダに言語間で共有する定数を集約する

Swiftの言語機能を利用したAPI設計

API . Topic . getDailyRankings(limit, offset) { // ... } API . Search . searchByName(keyword) { // ... }

Nested Classを使ったモデルクラスの宣言方法

struct API { static let version = " v1 " // 他の実装 }

extension API { class Topic { // 他の実装 } }



Genericsを使ったモデル生成処理の共通化

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プロトコル

extension API { class TopicData : JsonInitializable { required init ?(sJson: SwiftyJson) { title = sJson[ " title " ] . string if ! isValid { return nil } }

var isValid: Bool { return title != nil } }



subJsonパラメータ

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! " ... } }

Amebaアプリは2009年にストアにリリースされ、5年以上も運用されているプロジェクトです。コードベースは巨大で、その全てがObjective-Cで実装されていました。こういったケースではという判断をしがちで、Objective-Cでの開発を継続することが多いのではないかと思います。しかし、昨年10月という決定をしました。前述したフルネイティブ化していく大規模改修が昨年末からスタートすることが決定しており、アプリの基盤部分をSwiftへリプレースするにはベストなタイミングだと考えたためです。数年後のiOSアプリ開発の状況を想像すると、Swiftで実装することが主流になっているはずで、Objective-Cで実装し続けること自体が技術的負債を生む行為になると考えたからです。とはいえ、Amebaアプリは弊社随一のユーザ規模を誇ります。そのアプリでSwiftを採用するのは大きな決断でしたが、Objective-CやCocoaのフレームワークとの互換性が担保されている点が決め手となりました。Amebaアプリのコードベースは巨大なため、数ヶ月程度では全てをSwiftで実装し直すことは不可能です。この互換性により、完全Swift化までの過渡期として、新しく追加するSwiftのコードとこれまでのObjective-Cで実装された資産を共存させることができるからです。AmebaアプリのiOSチームには常時3、４名のメンバーが所属しています。このくらいの人数がいると、どうしても担当者によって設計や実装がバラバラになりがちです。メンバー間で認識を合わせて効率的に開発を進めるため、次の項目をルール化しました。Swiftは言語仕様として強力な型推論を持ち、型の宣言を省略することが可能です。また、Closureなどでは記述を大幅に簡素化して実装可能です。この辺りは人によって好みが出やすく、ルール化しないと統一感がなくなってしまいます。そこで下記のコーディング規約を採用し、原則としてこの規約に則ることにしました。新しいクラスはSwiftで実装することになりますが、現在はSwiftへの移行の初期段階のため、連携するクラスは既存のObjective-Cで実装されていることが大半です。こういった場合、Objective-Cとの連携を意識してしまうと、Swiftにしかない言語機能をどうしても使いにくくなってしまいます。そのため、原則としてObjective-Cとの連携は考慮せず設計・実装を行うようにしました。Objective-Cとの互換性がないなどの言語機能を利用したことにより、既存のクラスと連携できないようなこともありましたが、その場合はむしろ積極的に既存のクラスをSwiftで書き直すようにしました。とはいえ、Objective-Cの資産と連携せざる負えないことがあります。（よくないことなのですが、Objective-Cのクラスの依存関係が複雑すぎてサクッとSwiftに書き換えることができない 等々。長いこと運用している場合、ありますよね。こういうこと。）幸いAmebaアプリでは、で使う文字列定数をSwift/Objective-C間で共有できれば連携できる形になっていました。そのため、Objective-CとSwift間の定数ブリッジ用ヘッダを一つ用意し、このヘッダに言語間で共有する定数を集約することにしました。この方法はSwiftとObjective-Cが混在する過渡期の暫定対応と位置付けております。今後、既存クラスの書き換えが進み、そもそも定数を共有する必要がなくなれば、削除する予定です。Objective-Cとの連携を意識しない方針により、各所でSwiftならではの設計を採用しました。Webサービスのクライアントアプリによくある表示用データを取得する処理を例にして説明させて頂きます。次のサンプルは表示用データをWeb APIから取得するメソッドです。内部的にはHTTPレスポンスのJSONをパースし、モデルクラスへの変換まで行っておりますが、APIを呼び出す箇所ではとてもシンプルな記述にできます。また、のように.（ドット）で機能毎に区切られ、どういった機能のAPI呼び出しかを分かりやすくしています。API構造体に対して、各機能をextensionで分割し、`Nested Class`を使うことでName Space的な .(ドット) 区切りの見た目を実現しています。API構造体を`Class`にしなかったのは、Swift 1.1では言語仕様として`stored type property`が提供されておらず、`API.version`のような文字列定数を使うことができないからです。Swift 1.2から`Class`でも`stored type property`が利用できるので、そのうちClassに変更するかもしれません。の内部ではを呼び出しています。はモデルクラスが継承するプロトコルです。モデルクラスでは引数に渡される型の値からモデルクラスを初期化します。(※戻り値がnilになることもあるInitializerです。)にしているのは、引数に想定しない値が渡されることを考慮しているためです。Amebaアプリでは各モデルクラスにというを実装しており、内での場合は初期化失敗という意味でnilを返すようにしています。モデルクラスの初期化はHTTPレスポンスとして返されるJSONのルートから直接行わず、所定のキーに対応する値を使いたいことがあります。この問題に対応するためにクロージャを用意しました。ではとしてを指定しており、Topic Dataの"data"キーの値を使って初期化するようにしています。

以上がざっくりとしたAPI設計の内側です。

Swiftの言語機能を利用することでObjective-Cを使った場合と比較すると、かなりスッキリと実装できることがお分かりになるかと思います。





Swiftライブラリ

ライブラリの利用方法

まとめ

Objective-Cのコードを使いたくないという考えが根底にありますので、Swiftで実装されているライブラリを使うようにしました。採用したSwiftのライブラリは次の通りです。AFNetworkingでおなじみの Mattt Thompson さんが開発しているネットワークライブラリです。Swiftの綺麗なシンタックスとメソッドチェインを利用してHTTP関連の処理をスッキリと書くことができます。Swiftは型に厳しいのでJSONのパース処理は非常に面倒くさいのですが、このライブラリを利用するとシンプルにJSONを扱えるようになります。画像とJSONのキャッシュライブラリです。現在AmebaアプリではJSONのキャッシュ機能のみ利用しています。もともとは画像のキャッシュ機能も利用したかったのですが、導入時にはまだ画像キャッシュ機能が不安定だったため、利用を見送りました。画像キャッシュにはObjective-Cの SDWebImage というライブラリを利用しています。 @nghialv が開発しているライブラリです。プロトコルを実装することなく簡単に使うことができます。を使った画面では、その画面仕様の複雑さに比例してUIViewControllerが肥大化してしまいます。このライブラリを使うと、上でを実装する必要がなくなるため、肥大化問題をうまく解決できます！ @dekatotoro さんが開発しているPullToRefreshライブラリです。 @dekatotoro さんが開発しているスライドメニューライブラリです。現在、AmebaアプリはiOS 7.0以上をサポートしており、SwiftのライブラリをDynamic Frameworkとして使うことができません。。。そのため、各ライブラリはを使ってプロジェクトに追加しています。通常、外部リポジトリを依存管理する場合はでいいと思います。Amebaアプリの仕様上、一部のライブラリを修正する必要があったため、全ライブラリの取り込みにを使うことにしました。こんなことをせずに、単純にCocoapodsで依存管理できる日が来ることを心待ちにしております。AmebaアプリにおけるSwiftへ移行するための取り組みを紹介させて頂きました。Swiftに移行したことによる苦労（ビルド遅い、Swift Optimize Optionにより挙動の違い発生するなど）はありましたが、チームメンバーは総じてObjective-CからSwiftに移行したことに満足しています。既存プロジェクトの場合、移行のタイミングを見定めるのは非常に難しいですが、Xcode 6.3ではSwift関連の改善が多く盛り込まれますので、ぜひ検討してみてはいかがでしょうか？予想しているより、移行へのハードルは高くないと思いますよ！長文・乱文の中、最後までお読み頂きありがとうございました。今回のエントリが、Swiftへの移行に興味がある方の参考になれば幸いです。