Xcode
iOS
Swift
XCode7
EmbeddedFramework
SwiftDay 1

Embedded Framework使いこなし術

More than 1 year has passed since last update.

iOS 8・Xcode 6から、Embedded Frameworkが使えるようになりました

その導入法について書かれた記事などはよく見かけます("ios embedded framework" でググってください)が、実践的な説明・ノウハウ系はあまり見たことが無かったので、ご紹介します。

以下の2本+ステルス開発中の1本で1年程度の利用経験があります。

というわけで以下、Embedded Frameworkについて語っていきます。


利点

Staticライブラリというけっこう前からある別の仕組みもありますが、以下の利点全てを享受出来るのはEmbedded Frameworkです。


コード共有

最近、iOS本体アプリ以外にもApp ExtensionsやApple Watchなどターゲット(以下、メインターゲットと表現)が増えてきました。

もし、Embedded Frameworkを使わない場合、共有したいソースコード自体を複数ターゲットに紐付けることになります。

動作的には基本的に一緒になるはずですが、その方法では以下のデメリットが発生します。


  • ビルドが別途走るので時間かかる

  • ビルド生成物が重複するので、アプリサイズが増える

共有したいソースファイルをEmbedded Frameworkとすることで、同一のフレームワークを複数ターゲットが参照・利用することが可能になり、上記問題が解決します。

なので、メインターゲットが複数の場合は、基本的に共通部分をEmbedded Frameworkにまとめることをオススメします。


レイヤー分割・依存関係が強制される

こちらは設計の絡む話です。

後述のEmbedded Frameworkへの分け方を見た方が伝わりやすいかもしれません。

今まで、外部ライブラリ参照以外は、ベタに1つのプロジェクトに各種クラスを入れつつ、それをグループやフォルダで分ける、みたいなやり方をしている人が多いのではないでしょうか。

これでも依存関係がごっちゃになったりすることを気をつければ何とか防げるかもしれませんが、仕組み的に整えておいた方が確実です。

例えば、プロジェクト設定で以下のようにしておけば、例えばライブラリBがライブラリCに依存するような処理を書くことを強制的に防ぐことが可能となります。


  • ライブラリA: 依存無し

  • ライブラリB: ライブラリAに依存

  • ライブラリC: ライブラリA・ライブラリBに依存

メインターゲットがこれらライブラリA・B・Cを参照する構成の時、もちろんライブラリからはメインターゲットを参照出来ないので、相互に弄ってコードがぐちゃぐちゃになることも防げます。

また、このようにキレイに分けて呼び出しが限定的になっていると、このクラス・処理はどこに入れるべきかな?とより真剣に悩んだりして良いなと思っています。


ビルド時間短縮

Objective-C時代から、大規模なアプリになっていくとビルド時間がかかるのが気になることがありましたが、Swiftでより顕著になったかと思います。

(最新のXcode 7・Swift 2ではかなり改善されていますが。)

Swift 1.2で差分ビルドが導入されてビルドが速くなりましたが、Embedded Frameworkに分けていると、この恩恵を受けやすいです。

「画面側のコードを1行変えただけなのに全体にビルドかかっちゃった(´・︵・`)」みたいなことが、かなり減ります。

先日、Wantedlysusieyyさん達とこのあたりの話しましたが、以下のビルド時間問題の改善にもなるかもしれません。

メッセージングアプリSync開発の舞台裏(iOS) - Wantedly Engineer Blog

メインターゲット実行時に差分ビルドが効きやすく速くなるだけでなく、特定のEmbedded Frameworkのテスト実行なども軽快に出来て良いです。

ただ、これはWhole module optimizationをオンにしていると効かないので、デフォルトのオフにするように気をつけます。

Whole module optimizationがオンになっていると、差分が少しでもリビルドして全体最適化がされるようになります。

オススメの設定は、Debugビルドではオフで、Releaseビルドではオンです。

(ただし、実行時の挙動が稀に変わることあるので、Releaseビルドで要テスト)

そして、本記事を書きながら気づいたのですが、Whole module optimizationは以前はビルド設定の独立した項目でしたが、Xcode 7.1.1で以下のようにSwift最適化レベルとセットになっていました。

Screen Shot 2015-11-23 at 6.59.14 PM.png

セットになっているとはいえ、ちゃんと以前の設定を引き継ぎつつ変更されていたので多分大丈夫だと思いますが、念のため以前の設定が保たれているかチェックした方が良いかもしれません。

デフォルトだとReleaseビルドの設定はFast [-O]ですがその下の選択肢を選ぶとWhole module optimizationがオンになって下記のような挙動となります。


名前空間が分かれる

これもSwiftだけの利点で、Objective-Cでは当てはまらないようです。

importの有無や、モジュール名を明示的に指定することなどにより、呼び出したいクラスなどを制御出来ます。

Swift の Embedded Framework と namespace

フレームワーク間で名前の衝突が発生してもコンパイルエラーになったり、その回避のために呼び出し先を明示出来たりと、危なっかしかったObjective-Cよりかなり良い感じですね( ´・‿・`)

とはいえ、JavaやC#など、概ねフォルダ単位レベルで名前空間が分かれる感じの粒度では無いですよね(´・ω・`)

細かく分ける用途だと、Nested Typeなどですかね。

iOS - Swiftで名前空間を利用する - Qiita

上の例のようなクラスじゃなくてstructenumを使ったソースもよく見かけます。

Namespaced constants in Swift · Jesse Squires


Embedded Frameworkへの分け方の例

特に活用してなかったり、あるいは共通部分(Commonなど)を1つ作って、iOSアプリ・App Extensions・Apple Watchアプリなどから参照する、というやり方が世間で多い気がします。

(何となく話したり目にする記事からの印象です)

ただ、僕は上記のレイヤー分割・依存関係が強制されるビルド時間短縮のメリットを最大限活用するために、もう少し細かく分けています。

分け方の基準としては、「レイヤーとして分けるべき単位」で考えています。

(この基準に従ったとしても、人によって結果は異なると思っていますが。)

元々、.NETメインで開発していましたが、その感覚だとソリューションの下に関連プロジェクトをいくつか作りますが、その感覚でやっています。


  • 参照元のメインターゲット(以下など)で、いわゆるUI層


    • iOSアプリ

    • App Extensions

    • Apple Watchアプリ




  • Library




  • WebApiClient


    • サーバーとの通信処理周り

    • 内部的にAlamofireSwiftyJSONなど使いつつ、インターフェースはSwiftTaskのみに依存するようにしてます




  • Model


    • いわゆるモデル層

    • モデル定義・ロジック周りはここに集約


    • LibraryWebApiClientに依存

    • 各種サービスクラスを定義して、内部的にWebApiClientを使いつつ、永続化など経て、UI側に結果を返す



ちなみに、Libraryは開発している各アプリで同一のものを参照した方がキレイだなと思いつつ、Swift仕様がまだ変更多いことや、他のアプリに影響を与えずにささっとコード改善したい時などあり、別定義にしちゃっています。

Swiftの仕様変更が落ち着いたり、Libraryが成熟してきたら同一のものを参照する方式に変えようかなとも思っています。


躓きどころ

本質的なデメリットは無いに等しいと思っていますが、けっこう躓きポイントがあるので、実質的にデメリットとなり得るかもしれません。

僕はただでさえSwiftやXcodeバージョン間の差異や不安定さ真っ只中で、Embedded Frameworkも活用してたのでたまに悩ましかったですが、今はもうかなり安定してきているのであまり問題無い気がします。


CocoaPodsライブラリインストール

以前は、Embedded Framework内でCocoaPodsライブラリを使えるようにすることがうまく出来ず(コンパイルエラー・実行時エラーなど)、制約として諦めつつ開発していましたが、最近は下記方法でうまくいっています。

(以前から問題無かったしれませんが、僕はうまく出来ずに困っていました)



  • use_frameworks!指定でSwiftクラスからimport ライブラリ名でインポートする方式にする

  • メインターゲット + 利用する複数ターゲット向けにpod指定する


    • メインターゲット直接使用していなくてもメインターゲット向けにもインストール必要の模様(インストールしないと実機実行時にdyld: Library not loadedでクラッシュしました)



結果、こんな感じになりますが、手探りなのでベターな書き方あったら教えてください。

platform :ios, '8.0'

use_frameworks!

def common
pod 'FBSDKLoginKit'
end

# メインターゲット
common
pod 'SVProgressHUD'

target 'Model' do
common
end

ただ、Google Analyticsuse_frameworks!指定での利用時にコンパイル通らず、直接取り込みしています(´・︵・`)


@kishikawakatsumi さんに、ネストさせて共通のPodをまとめる書き方を教えていただけました😋

本記事の @kishikawakatsumi さんのコメントをご覧くださいヽ(・ω・`)


Carthageライブラリインストール


メインターゲット直接使用していなくてもメインターゲット向けにもインストール必要の模様


こちらも、CocoaPodsと同様、これに気をつける必要があります。

ただ、それ以外は特にハマったこと無いですね。

メインターゲットでは、CarthageライブラリEmbedded Binariesに放り込むとLinked Frameworks and Librariesにも自動追加されますが、Embedded FrameworkターゲットにはEmbedded Binariesが無く、手動でLinked Frameworks and Librariesに放り込む感じになります。

メインターゲット:

Screen Shot 2015-11-30 at 5.16.07 PM.png

Embedded Framework:

Screen Shot 2015-11-30 at 5.16.18 PM.png


ビルド設定

ビルド設定が原因でたまにビルドやアーカイブに失敗することがありますが、ちゃんとエラー文を読めば対処容易なものがほとんどでした。


  • bitcode設定



  • 署名周りで引っかかった記憶がありますが以下でOKかと思います


    • Provisioning Profile: Automatic

    • Code Signing Identity


      • Debug: iOS Developer

      • Release: iPhone Distribution






実例


FireFox iOS版

Firefox web browser on the App Storeのソースが公開されています( ´・‿・`)

こちらのソース見たら、Embedded Frameworkが活用されていました。

複数個使っている実コード例は貴重だと思うので、ご参考にヽ(・ω・`)


  • メインターゲット



    • ClientSendToViewLater



  • Embedded Framework


    • Shared: 上記のLibrary相当かな

    • Storage: 永続化層

    • Account: 上記のModel相当かな

    • Sync: 上記のModel相当かな

    • ReadingList: 上記のModel相当かな



上で書いたEmbedded Frameworkへの分け方の例とは少し違いますが、何かしらのポリシーを持って適当な粒度でレイヤー分けれていれば良いと思っています。

Screen Shot 2015-11-25 at 2.18.23 PM.png

分け方が違うとはいえ、こういう大手のアプリが同じような構造でアプリを組んでいることが分かって良かったです。


As of August 28, 2015, this project requires Xcode 7 beta 6.


ビルドしてみたりしたかったのですが、この制約のせいか、Storageのビルドで詰まってしまって出来ずでした(´・ω・`)

今手元にXcode 7.1.1しか無くて、環境用意するのも面倒だったので…。

他にもソース読むと色々発見ありそうですʕ ·ᴥ·ʔ


というわけで、是非活用していきましょう!

ネット上にもノウハウやトラブル解決事例が少ない状態なので、もっと利用が活発になれば僕も嬉しいです( ´・‿・`)