koogawa blog

iOS、Android、foursquareに関する話題

【iOS】既存のソースコードを Embedded Framework に切り出して複数のターゲットから利用する

こんにちは。koogawa です。

さて、一つのプロジェクトで複数のアプリを開発していると、共通部分をフレームワークに切り出して再利用したくなりませんか?

私はなります。

というわけで、今回は Xcode 6 から導入された Embedded Framework という仕組みを使って、フレームワークを作成する方法を紹介します。

目次

実行環境

プロジェクト構成

A と B、2つのターゲットがあり、ターゲットAの Client クラスをフレームワークに切り出して、ターゲットA、Bの両方から利用可能にするケースを考えます。

f:id:koogawa:20180412132654p:plain

Xcode 上は次のようになっています。

f:id:koogawa:20180412133207p:plain

ターゲットAのソースファイルは A ディレクトリに、ターゲットBのソースファイルは B ディレクトリに格納します。

f:id:koogawa:20180412133218p:plain

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」を選択すると、次の画面が表示されます。

f:id:koogawa:20180412133006p:plain

Cocoa Touch Framework」を選択します。

f:id:koogawa:20180412133040p:plain

「Product Name」は "Client" にしておきます。他の項目を適切なものにセットして「Finish」を押しましょう。

f:id:koogawa:20180412134559p:plain

Project Navigator に「Client」が追加されたと思います。このディレクトリにフレームワークのソースファイルを追加していきます。

ソースファイルを追加

Client.swift をフレームワークに切り出したいので、ソースファイルをドラッグアンドドロップします。

f:id:koogawa:20180412133702g:plain

切り出したクラスやメソッドは、外からもアクセスできるように 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 を追加します。

f:id:koogawa:20180412134639p:plain

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 してビルドしてみます。ビルドターゲットを切り替えるのを忘れずに。

f:id:koogawa:20180412134713p:plain

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:サンプルプログラムにつき、実用性は全く考えておりません