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行変えただけなのに全体にビルドかかっちゃった(´・︵・`)」みたいなことが、かなり減ります。
先日、Wantedlyのsusieyyさん達とこのあたりの話しましたが、以下のビルド時間問題の改善にもなるかもしれません。
メッセージングアプリSync開発の舞台裏(iOS) - Wantedly Engineer Blog
メインターゲット実行時に差分ビルドが効きやすく速くなるだけでなく、特定のEmbedded Frameworkのテスト実行なども軽快に出来て良いです。
ただ、これはWhole module optimization
をオンにしていると効かないので、デフォルトのオフにするように気をつけます。
Whole module optimization
がオンになっていると、差分が少しでもリビルドして全体最適化がされるようになります。
オススメの設定は、Debugビルドではオフで、Releaseビルドではオンです。
(ただし、実行時の挙動が稀に変わることあるので、Releaseビルドで要テスト)
そして、本記事を書きながら気づいたのですが、Whole module optimization
は以前はビルド設定の独立した項目でしたが、Xcode 7.1.1で以下のようにSwift最適化レベルとセットになっていました。
セットになっているとはいえ、ちゃんと以前の設定を引き継ぎつつ変更されていたので多分大丈夫だと思いますが、念のため以前の設定が保たれているかチェックした方が良いかもしれません。
デフォルトだとReleaseビルドの設定はFast [-O]
ですがその下の選択肢を選ぶとWhole module optimization
がオンになって下記のような挙動となります。
- 最適化のためにリビルドが走りやすくなってビルド時間はかかる
- 一方、文字通り最適化がかかるので実行パフォーマンスは良くなる
名前空間が分かれる
これもSwift
だけの利点で、Objective-C
では当てはまらないようです。
importの有無や、モジュール名を明示的に指定することなどにより、呼び出したいクラスなどを制御出来ます。
Swift の Embedded Framework と namespace
フレームワーク間で名前の衝突が発生してもコンパイルエラーになったり、その回避のために呼び出し先を明示出来たりと、危なっかしかったObjective-C
よりかなり良い感じですね( ´・‿・`)
とはいえ、JavaやC#など、概ねフォルダ単位レベルで名前空間が分かれる感じの粒度では無いですよね(´・ω・`)
細かく分ける用途だと、Nested Type
などですかね。
iOS - Swiftで名前空間を利用する - Qiita
上の例のようなクラスじゃなくてstruct
・enum
を使ったソースもよく見かけます。
Namespaced constants in Swift · Jesse Squires
Embedded Frameworkへの分け方の例
特に活用してなかったり、あるいは共通部分(Common
など)を1つ作って、iOSアプリ・App Extensions・Apple Watchアプリなどから参照する、というやり方が世間で多い気がします。
(何となく話したり目にする記事からの印象です)
ただ、僕は上記のレイヤー分割・依存関係が強制されるやビルド時間短縮のメリットを最大限活用するために、もう少し細かく分けています。
分け方の基準としては、「レイヤーとして分けるべき単位」で考えています。
(この基準に従ったとしても、人によって結果は異なると思っていますが。)
元々、.NETメインで開発していましたが、その感覚だとソリューションの下に関連プロジェクトをいくつか作りますが、その感覚でやっています。
-
参照元のメインターゲット(以下など)で、いわゆるUI層
- iOSアプリ
- App Extensions
- Apple Watchアプリ
-
Library
- アプリ関係無く使うであろう共通クラス・関数など定義
- UIKit・Foundationクラスを拡張してよく使う処理をメソッドとして生やしたり
- ロガーなども
- http://qiita.com/mono0926/items/c53b2e46e51a0b0bbf06#embedded-frameworkを利用 でも使用について言及
- アプリ関係無く使うであろう共通クラス・関数など定義
-
WebApiClient
- サーバーとの通信処理周り
- 内部的にAlamofire・SwiftyJSONなど使いつつ、インターフェースはSwiftTaskのみに依存するようにしてます
-
Model
- いわゆるモデル層
- モデル定義・ロジック周りはここに集約
-
Library
・WebApiClient
に依存 - 各種サービスクラスを定義して、内部的に
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 Analyticsはuse_frameworks!
指定での利用時にコンパイル通らず、直接取り込みしています(´・︵・`)
@kishikawakatsumi さんに、ネストさせて共通のPodをまとめる書き方を教えていただけました😋
本記事の @kishikawakatsumi さんのコメントをご覧くださいヽ(・ω・`)
Carthageライブラリインストール
メインターゲット直接使用していなくてもメインターゲット向けにもインストール必要の模様
こちらも、CocoaPodsと同様、これに気をつける必要があります。
ただ、それ以外は特にハマったこと無いですね。
メインターゲットでは、CarthageライブラリEmbedded Binaries
に放り込むとLinked Frameworks and Libraries
にも自動追加されますが、Embedded Framework
ターゲットにはEmbedded Binaries
が無く、手動でLinked Frameworks and Libraries
に放り込む感じになります。
ビルド設定
ビルド設定が原因でたまにビルドやアーカイブに失敗することがありますが、ちゃんとエラー文を読めば対処容易なものがほとんどでした。
- bitcode設定
- 無効にする場合、
Embedded Framework
ターゲットのも無効に(Xcode 7.1までは弄らずOKだった) - Swift - Embedded Framework で Carthage を使ったさいの問題と回避策 - Qiita など情報ありますが、僕は今のところ有効にする拘り無いので無効にしちゃっています
- 無効にする場合、
- 署名周りで引っかかった記憶がありますが以下でOKかと思います
- Provisioning Profile: Automatic
- Code Signing Identity
- Debug: iOS Developer
- Release: iPhone Distribution
実例
FireFox iOS版
Firefox web browser on the App Storeのソースが公開されています( ´・‿・`)
こちらのソース見たら、Embedded Frameworkが活用されていました。
複数個使っている実コード例は貴重だと思うので、ご参考にヽ(・ω・`)
- メインターゲット
-
Client
・SendTo
・ViewLater
-
- Embedded Framework
- Shared: 上記の
Library
相当かな - Storage: 永続化層
- Account: 上記の
Model
相当かな - Sync: 上記の
Model
相当かな - ReadingList: 上記の
Model
相当かな
- Shared: 上記の
上で書いたEmbedded Frameworkへの分け方の例とは少し違いますが、何かしらのポリシーを持って適当な粒度でレイヤー分けれていれば良いと思っています。
分け方が違うとはいえ、こういう大手のアプリが同じような構造でアプリを組んでいることが分かって良かったです。
As of August 28, 2015, this project requires Xcode 7 beta 6.
ビルドしてみたりしたかったのですが、この制約のせいか、Storage
のビルドで詰まってしまって出来ずでした(´・ω・`)
今手元にXcode 7.1.1しか無くて、環境用意するのも面倒だったので…。
他にもソース読むと色々発見ありそうですʕ ·ᴥ·ʔ
というわけで、是非活用していきましょう!
ネット上にもノウハウやトラブル解決事例が少ない状態なので、もっと利用が活発になれば僕も嬉しいです( ´・‿・`)