オールアバウトTech Blog

株式会社オールアバウトのエンジニアブログです。

アップデートし続けるアプリのSwift移行

f:id:allabout-techblog:20170329090435p:plain

はじめに

初めまして!オールアバウトの @morimorimです。 2016年度入社新卒エンジニアの連載企画第三本目として、CafeSnapというアプリをObjective-CからSwiftへ移行している話をしたいと思います。

CafeSnapとは

CafeSnapとは、日本全国にある個性の光るカフェを探すことができる写真共有型アプリです。 タイムラインに流れている写真や、今いる場所、メニュー、特徴など多彩な条件からカフェを探すことができます。 ぜひご利用ください!

f:id:allabout-techblog:20170316230704j:plain:w200f:id:allabout-techblog:20170316230653j:plain:w200

なんでSwift移行を決めたの?

Swift移行しようとした理由は主に3つあります。

1つはSwiftの学習コストの低さと開発効率の良さです。 SwiftはObjective-Cに比べ学習しやすく、実際Objective-Cを書けるようになるまでの時間よりSwiftのほうが短いです。 また、構文の書き方も簡単になっており、記述するのがとても楽になっています。

例えば下記のようにクラスをインスタンス化する時は、型推論のおかげであらかじめ型を指定する必要がないのと、[ ]を書かなくて良くなり、一行一行のコーディングの負担が地味に減りました。

// Objecitve-C
Hoge* hogeInstance = [Hoge new];

// Swift
let hogeInstance = Hoge()

2つ目の理由は他のプロジェクトのためにSwiftの知見をためておくことです。 僕の入社当初に社内で立ち上がっていたプロジェクトは開発言語としてSwiftを利用する予定でしたが、当時社内でSwiftを触ったことのあるエンジニアはあまりいない状況でした。 他のプロジェクトでもSwiftを利用する知見を社内でためておくためにも、先行してCafeSnapで移行を進めることになったのです。

最後の理由は将来のアプリ開発現場を考えてのことです。 おそらく将来リリースされるアプリのほとんどはSwiftでできていること、開発現場で使用されるライブラリはSwiftで書かれた良いものが増えていくと予想しました。

開発効率を維持し続けるならiOSアプリにとって、Swiftへの移行はほぼ必須であると感じたからです。

CafeSnapでやっているSwift移行

CafeSnapでは1つのプロジェクト上でObjective-CとSwiftを混在させ、相互利用させる移行方法を選択しました。 CafeSnapは2014年9月にローンチされたので、2年分の資産が詰まったアプリです。 コードはObjective-Cで書かれており、入社当初は頑張って読んだものです(遠い目)。

そんな2年分のObjective-Cのコードを残しつつ、徐々にSwiftへ移行していけるのが相互利用方式を選択した理由です。

f:id:allabout-techblog:20170316230009p:plain

すでに実装されているコードを機能単位でSwiftに移していくイメージです。 例えば、機能AをSwift移行するとしたら、機能A内で使用するViewController、CustomView、Utility、通信クラスなどのクラスをSwiftに書き換えます。

これにより1つのプロジェクト上でSwiftとObjective-Cのコードを共存させながら移行を進めることができます。 ちなみに相互利用方式を導入する上で必要不可欠なBridging-Header作成は下記のサイトがとてもわかりやすかったです。 1回目に間違えてBridging-Headerを作成し損ねた場合のことも書いてあるので安心です。

[Swift] プロジェクトに Bridging-Header.h ファイルを追加する

やってよかったこと(やりたいこと)

個人的にやってよかったこと(やりたいこと)は主に下記の3つです。

可読性、実装しやすさの向上

Objective-Cを初めて触った時、メソッド名どこ!? とか、どれがプロパティでどれがメソッドか全くわからないという 怒り 読みづらさがありました。

ですが、Swiftだとfunc、varやletキーワードが使用できるようになり、参照するときもプロパティとメソッドは指定の仕方が完全に異なるのでかなり読みやすくなりました。 また、配列を作るときもNSArrayとNSMutableArrayを使い分ける必要がなくなったのも個人的に ストレス手間がなくなって大変嬉しかった記憶があります。

Optionalが便利!

例えば下記のような.plistのパスを取得するコードがあったとします。

NSBundle *bundle = [NSBundle mainBundle];
NSString *path = [bundle pathForResource:@"hoge" ofType:@"plist"];
NSDictionary *dic = [NSDictionary dictionaryWithContentsOfFile:path];

例えばhoge.plistが存在しないとしたら、pathには何が入ると思いますか? nullが入ります。ただnullが入ってしまったとしてもそのまま動き続けてしまった場合、確実に表示がおかしなことになります。

ここでSwiftのOptionalという型の出番です。 Optionalについて詳しい説明はどこよりも分かりやすいSwiftの"?“と”!"などいろんなところで語られているので割愛します。 上記のコードを下のように書きます。!のところがアンラップと呼ばれる箇所ですね。

let bundle = NSBundle.mainBundle()

// fatal error: unexpectedly found nil while unwrapping an Optional value
let path = bundle.pathForResource("hoge", ofType:"plist")!
let dic = NSDictionary(contentsOfFile: path)

アンラップすることにより、pathが不正(nil)ならアプリをクラッシュさせることができるので、原因の発見がしやすくなります。

Protocol Extensionの活用

Protocol Extensionは理解した時すごく感動しました。 最近ようやく存在に気づいたのでこれからはガンガン使っていきたいです。

詳細はProtocol Extensionでググればたくさん参考資料が出てきますが、 簡単に言ってしまえばクラスを進化させて強くするのではなく、適切な装備をつけてあげようという概念です。 具体的には下記のような感じですね。下記ソースはSwift2のProtocol Extensionsとクラス継承を比較するを参考にさせていただいています。

継承の場合

継承の場合、親クラスを実装してその中に共通処理を実装していくことになります。 ViewControllerがメンテナンスとロギングの機能を共通で持つことを想定して実装します。

class BaseViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        if Mirror(reflecting: self).subjectType !=  ViewControllerB.self {
            checkMaintenance()
        }
        printLog()
    }

    func checkMaintenance() {
        // check
    }

    func printLog() {
        // logging
    }
}

class ExampleAViewController: BaseViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
    }
}

class ExampleBViewController: BaseViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
    }
}

良いところは共通処理を書かなくて済むことですが、ここに複雑な要件、例えばAはメンテナンスさせるけど、Bはさせないようにしたいという場合は、上記のように親クラスの中で分岐を書く必要があります。 なので親クラスはどんどん太っていってしまい、読みづらくなってしまいます。

Protocol Extensionの場合

一方Protocol Extensionはメンテナンスとロギングを別々のprotocolで定義し、それぞれ拡張します。あとは必要な機能をそれぞれのViewControllerで継承させます。

protocol Loggable {
    func printLog()
}

extension Loggable where Self: UIViewController {
    func printLog() {
        // logging
    }
}

protocol MaintenanceCheckable {
    func checkMaintenance()
}

extension MaintenanceCheckable where Self: UIViewController {
    func checkMaintenance() {
        // check
    }
}

class ExampleAViewController: MaintenanceCheckable, Loggable {
    override func viewDidLoad() {
        super.viewDidLoad()
        checkMaintenance()
        printLog()
    }
}

class ExampleBViewController: Loggable {
    override func viewDidLoad() {
        super.viewDidLoad()
        printLog()
    }
}

Protocol Extensionの場合、1つの機能を必要に応じてつけたり外したりできるので、AにはつけてBにはつけないといった一部の処理だけ利用したい場合に非常に柔軟に対応することができます。

発生した問題

さて、ここまでSwiftのメリットについて話してきましたが、メリットだけがあったわけではなく、問題もたくさん発生しました。 その中でも厄介だったものをいくつか紹介いたします。

人によって書き方がバラバラ

Swiftは1つ処理を実現するにも様々な書き方があります。 例えばInt型の配列を定義するとき、

var a: Array<Int> = [1, 2, 3]

と定義できますが、Swiftはとてもお利口さんなので、

var a: [Int] = [1, 2, 3]
var a = [1, 2, 3]

のように複数のパターンで書くことができます。 そのため、複数人で開発していると機能ごとに書き方が違うことがありました。 CafeSnapではAPIiOS版、Android版の開発を全員で行っていますが、それぞれ得意な言語と苦手な言語があり、あまりSwiftの開発に明るくない人もいます。 書き方がそれぞれ違うとコードレビューの時に「なんだこの書き方は?」となってしまうこともあり、無駄にコミュニケーションコストが発生してしまうことがありました。

そこでCafeSnapでは簡単なSwiftのコーディング規約を作成し、チームでSwiftのベースとなる書き方を統一することにしています。 下記はほんの一例ですが、

  • Swiftは型推論が行えるので、基本的に型を明示的に書かないようにする
  • メソッドの引数に指定するクロージャは1つだけなら外に出し、2つ以上は出さない
  • 明示的なself参照は必要な時(コンパイルエラーが出る時)だけ

などを決めています。

ライブラリが見つからない?

CafeSnapではこれまでの資産をそのまま用いることを選択したので、Objective-CとSwiftがお互い参照し合うことがあります。 最初は特に問題なかったのですが、移行を進めていくうちに少しずつ問題が発生するようになりました。 例えば、AppDelegateの処理をSwiftで使うためにBridging-Headerに追加し、Swiftで書いたprotocolを実装するためにCafeSnap-Swift.hをAppDelegate.hにインポートするとします。

f:id:allabout-techblog:20170327205545p:plain

そうすると、いくらクリーンしたり、パスを変えたりしてもCafeSnap-Swift.hがnot foundになってしまい、インポートできなくなってしまいました。 この問題が発生した時はAppDelegate.hへのインポートをやめ、Objective-Cでinterfaceを定義し、interfaceをAppDelegate.hへインポート、実装することにしています。

f:id:allabout-techblog:20170327205617p:plain

そもそも実装方法が良くないのかもしれませんが、この問題は未だに根本的な原因がわかっていません。 ご存知の方がいらしたらぜひ教えてください!

ビルド時間が徐々に遅くなってる

あれ?なんかビルドが遅い?と気がつくとビルドが5分経っても終わらなくなってしまいました。 とりあえず下記の方法で原因の調査&速度改善を図りました。

  • Xcprofiler を使って秒数計測して時間のかかっているファイルを捜索し修正
    → 12秒(3秒x4ファイル)改善できそう(まだやってない)
  • ビルドに使用するコアを1つ増やす(処理の並列化)
    → 20秒改善

CafeSnapの場合は、どれを行ってもそれほど大きな改善できていません。非常に悔しい限りです。 型推論をやめる、クロージャの引数の簡略化をやめる、など広範囲に及びそうな修正はまだ手付かずなので、今後はそちらも試そうと思っています。

おわりに

以上、CafeSnapでやっているSwift移行の話をさせていただきました。 こんなに大変な移行をなぜしなくてはいけないのか、時には心が折れそうになったこともあります。 そんな時はアプリのアップデートし続ける理由をいつも考えています。 ユーザは常に良いものを求めています。ユーザ満足度を高めるためにアプリは常にアップデートし続けなければなりません。 アップデートし続けるアプリにとって、将来の市場、開発効率を考えてもSwift移行は必要なことだと考えます。

(コードを見ていて気づかれた方もおられるかもしれませんが、現在やっているのは Swift2移行です。なので、実はこの後さらに Swift3移行が待っています。応援よろしくお願いいたします)

おまけ

Swift移行の参考になる記事