こんにちは。koogawa です。
さて、一つのプロジェクトで複数のアプリを開発していると、共通部分をフレームワークに切り出して再利用したくなりませんか?
私はなります。
というわけで、今回は Xcode 6 から導入された Embedded Framework という仕組みを使って、フレームワークを作成する方法を紹介します。
目次
実行環境
- Xcode 9.3
- Swift 4.1
プロジェクト構成
A と B、2つのターゲットがあり、ターゲットAの Client
クラスをフレームワークに切り出して、ターゲットA、Bの両方から利用可能にするケースを考えます。
Xcode 上は次のようになっています。
ターゲットAのソースファイルは A ディレクトリに、ターゲットBのソースファイルは B ディレクトリに格納します。
Client
クラスは API と通信し、レスポンスデータを String 型にして返す request
メソッドを持ちます。*1
import Alamofire class Client { init() { } func request(_ complete: @escaping (String?) -> Void) { Alamofire.request("https://httpbin.org/get").responseJSON { response in var responseString: String? = nil if let data = response.data { responseString = String(data: data, encoding: .utf8) } complete(responseString) } } }
見ておわかりの通り、通信ライブラリである Alamofire を利用しています。
ライブラリ管理は CocoaPods で行ないます。
Podfile:
source 'https://github.com/CocoaPods/Specs.git' platform :ios, '10.0' use_frameworks! target 'A' do pod 'Alamofire', '~> 4.7' end
ターゲットAでの作業
それでは Client
クラスをフレームワークに切り出していきましょう。
Cocoa Touch Framework を追加
Xcodeのツールバーから「File」 → 「New」 → 「Target」を選択すると、次の画面が表示されます。
「Cocoa Touch Framework」を選択します。
「Product Name」は "Client" にしておきます。他の項目を適切なものにセットして「Finish」を押しましょう。
Project Navigator に「Client」が追加されたと思います。このディレクトリにフレームワークのソースファイルを追加していきます。
ソースファイルを追加
Client.swift をフレームワークに切り出したいので、ソースファイルをドラッグアンドドロップします。
切り出したクラスやメソッドは、外からもアクセスできるように public
修飾子をつけておきます。
public class Client { public init() { } public func request(_ complete: @escaping (String?) -> Void) { Alamofire.request("https://httpbin.org/get").responseJSON { response in var responseString: String? = nil if let data = response.data { responseString = String(data: data, encoding: .utf8) } complete(responseString) } } }
インポートして使ってみる
ターゲットAから使ってみましょう。Client
を import するだけで使えるようになります。
import Client class ViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() let client = Client() client.request({ responseString in if let responseString = responseString { print(responseString) } }) } }
この時点でいったんビルドしてみると
/Path/To/Sample/Client/Client.swift:10:8: No such module 'Alamofire'
のようなエラーが出ると思います。どうやらフレームワークから Alamofire が読み込めていないようなので、Podfile を次のように書き換えてみましょう。
source 'https://github.com/CocoaPods/Specs.git' platform :ios, '10.0' use_frameworks! target 'Client' do pod 'Alamofire', '~> 4.7' end
再度 pod update
してビルドしてみましょう。今度はビルドが通りましたね。
今度は ⌘R
で Run してみましょう。
dyld: Library not loaded: @rpath/Alamofire.framework/Alamofire
おや、クラッシュしましたね。まだ Alamofire が読み込めていないようです。今度は Podfile を下記のように書き換えてリトライしてみましょう。
source 'https://github.com/CocoaPods/Specs.git' platform :ios, '10.0' use_frameworks! target 'A' do pod 'Alamofire', '~> 4.7' end target 'Client' do pod 'Alamofire', '~> 4.7' end
今度はクラッシュしませんでしたね!
このように CocoaPods を使う場合は、フレームワークだけでしか利用していないライブラリもメインターゲットにインストールする必要があるので注意してください。
ターゲットBでの作業
次はターゲットBからもフレームワークを使えるようにしていきましょう。
Embedded Binaries にフレームワーク追加
TARGET から「B」を選択し、General の中にある「Embedded Binaries」に Client.framework
を追加します。
Podfile 更新
ターゲットBにも Alamofire をインストールします。
source 'https://github.com/CocoaPods/Specs.git' platform :ios, '10.0' use_frameworks! target 'A' do pod 'Alamofire', '~> 4.7' end target 'B' do pod 'Alamofire', '~> 4.7' end target 'Client' do pod 'Alamofire', '~> 4.7' end
これでも動くのですが、ターゲットごとに同じライブラリ名を記述するのは冗長なので、次のように abstract_target
でまとめるのが良いでしょう。
source 'https://github.com/CocoaPods/Specs.git' platform :ios, '10.0' use_frameworks! abstract_target 'All' do pod 'Alamofire', '~> 4.7' target 'A' do end target 'B' do end target 'Client' do end end
インポートして使ってみる
Client
を import してビルドしてみます。ビルドターゲットを切り替えるのを忘れずに。
import Client class ViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() let client = Client() client.request({ responseString in if let responseString = responseString { print(responseString) } }) } }
ビルドできましたね。
これでターゲットBからもフレームワークが使えるようになりました!
トラブルシューティング
フレームワークがインポートできない場合は次のことを確認してみてください。
- フレームワークのクラス宣言やメソッドに
public
修飾子は付いていますか? - Xcode のキャッシュが原因の場合もあるのでクリーンビルドすると解決することがあります
- 再度
pod update
することで解決することもありました
さいごに
Embedded Framework を使って、フレームワークを作成する方法を紹介しました。少しだけハマりポイントもありましたが、意外と簡単だったのではないでしょうか。
また、今回は触れませんでしたが、Apple Watch アプリや Today Widget などの App Extentions を持つアプリの場合、メインターゲットと Extension 間のコード共有も Embedded Framework で可能になりますのでぜひ活用してみてください。
*1:サンプルプログラムにつき、実用性は全く考えておりません