今回は iOS 向けに Framework を作成する。
Frameworkとは?
前回のエントリでまとめたのでそちらをどうぞ。
Cocoaの日々: [Mac][iOS] Frameworkとは?
サマリー
今回は SampleKit という Framework を作成する。
中身はこんな感じ。
Xcodeプロジェクトはこんな感じになる。
ターゲットを2つを用意する。 Frameworkを作成するターゲット(上図の SampleKit.framework ターゲット)を使いビルドを開始すると、最初に SampleKit ターゲットを呼び出して各アーキテクチャ毎のライブラリ(*.a)を作成する。
そしてこれらを lipoコマンドでユニバーサル化した後、Frameworkのディレクトリを構成する。最後に ZIP圧縮して配布できるようにする。
Frameworkで公開したいヘッダファイル(*.h)はHeaders に入れておく。これらがビルド時に Framework/Headers へコピーされる。公開したくないヘッダファイルはここへは入れない(Classesなどへ入れる)。
ビルド時に使用する Framework名称とバージョン番号(ZIPファイル名に使用)は Info.plist ファイル内の値を使う[*1]。
[*1] ビルド設定(環境変数)は使わない
Frameworkを作成した後、動作確認の為にサンプルアプリケーションを作る。
Framework 作成手順
1. Xcode プロジェクト作成
まずは Xcode を開き、テンプレートから "Cocoa Touch Static Library" を選択する。名前は "SampleKit" とした。
こんなプロジェクトが生成される。
2. リソース準備
Resources グループを作成し、Info.plist ファイルを作成する。
Info.plist の中身はこんな感じ。
上記はMac OS X 用フレームワーク作成テンプレートのものをコピーして、それにドキュメントで推奨されていた項目を付け足した( Copyright, Get Info stringなど)。Bundle Versions string, short と Bundle name はこの後のシェルスクリプトで参照する。
3. ヘッダ用ディレクトリ準備
公開ヘッダ用のディレクトリを準備する。まず Xcodeで新規にグループを作成し "Headers" と名前をつける。
ただこれは Xcodeプロジェクト上だけで有効なもので自分でディレクトリを作成してひもづける必要がある。"Headers"の情報を開き、パスの「選択」を選ぶ。
ダイアログが開いたら左下の「新規フォルダ」ボタンを押して "Headers" というディレクトリを作成する。
このディレクトリを選択すると情報のパスの欄に "Headers" と表示される。
この Headersディレクトリに入っているヘッダファイル(*.h)を後ほど Frameworkディレクトリへコピーする。公開したくないヘッダファイルがある場合はここへ入れなければ良い。
4. ソースコード準備
動作確認用のクラスのソースコードファイルを Classes 配下へ追加する。
今回 SampleClass.h は公開したい(Frameworkに同梱)ので Headers 配下に配置しておく逆に公開したくない場合は Classesなどへ入れておく。
中身はこんな感じ。
@interface SampleClass : NSObject { } - (NSString*)helloString; @end
@implementation SampleClass - (NSString*)stringHello { return @"Hello mac!"; } @end
5. ターゲット準備
Framework作成用に新規ターゲットを作成する。ターゲットの上で右クリックして「追加」→「新規ターゲット」→ "Shell Script Target" を選択する。
ターゲット名を "SampleKit.framework" とする。
ターゲット内の「スクリプトを実行」を開き、ビルド用のシェルスクリプトを記述する。
ビルド用シェルスクリプト
#-------------------------------------------------------------------- echo "[0] Framework: Preparing ..." #-------------------------------------------------------------------- FRAMEWORK_NAME=$(/usr/libexec/PlistBuddy -c "Print CFBundleName" Info.plist) BUILD_TARGET_NAME=$FRAMEWORK_NAME FRAMEWORK_VERSION_NUMBER=$(/usr/libexec/PlistBuddy -c "Print CFBundleShortVersionString" Info.plist) FRAMEWORK_VERSION=A FRAMEWORK_BUILD_PATH="build/${BUILD_STYLE}-framework" FRAMEWORK_DIR="${FRAMEWORK_BUILD_PATH}/${FRAMEWORK_NAME}.framework" PACKAGENAME="${FRAMEWORK_NAME}.${FRAMEWORK_VERSION_NUMBER}.zip" #-------------------------------------------------------------------- echo "[1] Framework: Building libraries ..." #-------------------------------------------------------------------- xcodebuild -configuration ${BUILD_STYLE} -target ${BUILD_TARGET_NAME} clean xcodebuild -configuration ${BUILD_STYLE} -target ${BUILD_TARGET_NAME} \ -sdk iphonesimulator${IPHONEOS_DEPLOYMENT_TARGET} [ $? != 0 ] && exit 1 xcodebuild -configuration ${BUILD_STYLE} -target ${BUILD_TARGET_NAME} \ -sdk iphoneos${IPHONEOS_DEPLOYMENT_TARGET} [ $? != 0 ] && exit 1 echo "Framework: Cleaning framework..." [ -d "${FRAMEWORK_BUILD_PATH}" ] && rm -rf "${FRAMEWORK_BUILD_PATH}" #-------------------------------------------------------------------- echo "[2] Framework: Setting up directories..." #-------------------------------------------------------------------- mkdir -p ${FRAMEWORK_DIR} mkdir -p ${FRAMEWORK_DIR}/Versions mkdir -p ${FRAMEWORK_DIR}/Versions/${FRAMEWORK_VERSION} mkdir -p ${FRAMEWORK_DIR}/Versions/${FRAMEWORK_VERSION}/Resources mkdir -p ${FRAMEWORK_DIR}/Versions/${FRAMEWORK_VERSION}/Headers #-------------------------------------------------------------------- echo "[3] Framework: Creating symlinks..." #-------------------------------------------------------------------- ln -s ${FRAMEWORK_VERSION} ${FRAMEWORK_DIR}/Versions/Current ln -s Versions/Current/Headers ${FRAMEWORK_DIR}/Headers ln -s Versions/Current/Resources ${FRAMEWORK_DIR}/Resources ln -s Versions/Current/${FRAMEWORK_NAME} ${FRAMEWORK_DIR}/${FRAMEWORK_NAME} #-------------------------------------------------------------------- echo "[4] Framework: Creating library..." #-------------------------------------------------------------------- lipo -create \ build/${BUILD_STYLE}-iphoneos/lib${FRAMEWORK_NAME}.a \ build/${BUILD_STYLE}-iphonesimulator/lib${FRAMEWORK_NAME}.a \ -o "${FRAMEWORK_DIR}/Versions/Current/${FRAMEWORK_NAME}" #-------------------------------------------------------------------- echo "[5] Framework: Copying assets into current version..." #-------------------------------------------------------------------- cp Headers/*.h ${FRAMEWORK_DIR}/Headers/ cp Info.plist ${FRAMEWORK_DIR}/Resources/ #-------------------------------------------------------------------- echo "[6] Framework: Packaging framework..." #-------------------------------------------------------------------- cd ${FRAMEWORK_BUILD_PATH} zip -ry ${PACKAGENAME} $(basename $FRAMEWORK_DIR)(2011-09-05追記)
Xcode 4.1 から環境変数 BUILD_STYLE が使われなくなった。代わりに CONFIGURATION を使う。上記スクリプト内の ${BUILD_STYLE} をすべて ${CONFIGURATION} で置換する。
(2011-10-21追記)
Xcode 4.2 から SDK 5.0 のみの提供となった。この為、ターゲットを 4.3 としてビルドするとエラーになる。その場合は xcodebuild コマンドの引数から ${IPHONEOS_DEPLOYMENT_TARGET} を取り除く。
(修正前)xcodebuild -configuration ${BUILD_STYLE} -target ${BUILD_TARGET_NAME} \ -sdk iphoneos${IPHONEOS_DEPLOYMENT_TARGET} (修正後)xcodebuild -configuration ${BUILD_STYLE} -target ${BUILD_TARGET_NAME} \ -sdk iphoneos
(2011-10-21追記)
Xcode 4.2 でターゲットを 4.3 としてビルドするとエラーが出る場合がある。この場合はコンパイラを Apple LLVM compiler 3.0 から LLVM GCC 4.2 に切り替えると解消する(かもしれない)。
xcodebuild コマンドを使い実機(iphoneos)とシミュレータ(iphonesimulator)用のバイナリ(*.a) ファイルを作成する。その後、lipo コマンドで1つにまとめ(ユニバーサル化)Frameworkのディレクトリを構成する。最後はそのディレクトリを ZIP圧縮する。
6. ビルド
ビルドしてみよう。"SampleKit.framework" を選択しビルドを実行する。
※Device/Simulator は無視される。Debug/Release は有効。
フォルダを確認すると...
できた。
file コマンドでバイナリを確認してみる。
$ file SampleKit SampleKit: Mach-O universal binary with 3 architectures SampleKit (for architecture armv6): current ar archive random library SampleKit (for architecture armv7): current ar archive random library SampleKit (for architecture i386): current ar archive random libraryちゃんと arm6/7、i386用のライブラリが入っている。
Frameworkを利用するサンプル
作成した Framework を使うサンプルを作ってみる。View-basedなプロジェクトを作成し、先ほど作成した Frameworkを追加する。
Frameworkとして認識された。
Xcodeのウィンドウの左ツリーで SampleKit.frameworkを選択すると右側には内包している SampleClass.h が表示される。
コントローラに下記コードを追加する。
#import <SampleKit/SampleClass.h> : - (void)viewDidLoad { [super viewDidLoad]; SampleClass* obj = [[[SampleClass alloc] init] autorelease]; NSLog(@"%@", [obj stringHello]); }Framework 内のヘッダファイルは下記の形式でインポートする。
#import <フレームワーク名/ヘッダファイル名>
シミュレータで実行すると...
SampleKitClient[19559:207] Hello mac!デバッグコンソールに出た。その後、実機でも動作確認ができた。良さそうだ。
考察
"Frameworkとは?" の記事で触れた様に iOS の場合は Framework は静的ライブラリのプレースホルダとして使うのが一般的と思われる。
運用イメージ
この為、もし自作した Framework がリソース(nib,画像など)を含む場合は、利用側のプロジェクトのビルドフェーズでそれらをアプリケーションバンドル内へコピーして一緒に配布する必要がある。
なお仕組み自体は動的ライブラリの格納も可能なので、"Frameworkとは?"で取り上げた embed形態を取ることで実行時にリンクさせることもできる。この場合だと実行時に必要なライブラリしか読み込まれない為、フットプリント(メモリ消費)を減らせる可能性がある。
トラブルシュート
SDKが見つからないエラーが出た場合
スクリプト内のPATH環境変数設定で古い SDKのディレクトリを指定している場合など。
export PATH=/Developer/usr/bin:"$PATH"
明示的に指定する必要がなければ、この PATH設定を削除しておく。すると現在実行中の Xcodeの SDKのバージョンが使用される
ビルド中にエラー
ターゲットが見つからない場合などは、スクリプト内の xcodebuild コマンドで指定しているターゲットが正しく設定されていない可能性がある。
スクリプト内の xcodebuildコマンドのターゲットに指定しているのは、最初にテンプレートで作成されたターゲットである "SampleKit" である。このターゲット名を用意するのに Info.plist 内の "Bundle Name" を使用している。この名前とライブラリ(*.a)作成用のターゲット名が一致している必要がある。
シェルスクリプト内の該当箇所は下記になる。
#-------------------------------------------------------------------- echo "[0] Framework: Preparing ..." #-------------------------------------------------------------------- FRAMEWORK_NAME=$(/usr/libexec/PlistBuddy -c "Print CFBundleName" Info.plist) // ←これ BUILD_TARGET_NAME=$FRAMEWORK_NAME // ←
ヘッダが無いというエラーが出る
Headers ディレクトリにファイルが無いとこのエラーが出る。例えば追加したヘッダファイルのパスが間違っていた場合など。
この場合は該当するファイルが入っているフォルダを開く。
そしてファイルを Headers へコピーする。
すると Xcode 上でファイルが赤くなる。これは指定パス位置にファイルが無いことを示している。
そこで情報を開き、パスの「選択」ボタンを押して目的のファイルを選択する。
これでビルドが通る様になる。
ソースコード
GitHubからどうぞ。
Framework作成プロジェクト
SampleKit at 2010-11-19c from xcatsan's iOS-Sample-Code - GitHub
Frameworkを使うサンプル
SampleKitClient at 2010-11-19 from xcatsan's iOS-Sample-Code - GitHub
※ SampleKitClient 内で参照している SampleKit へのパスは私のPCのローカル環境になっています。ビルドする際には SampleKit を別途ダウンロードして、参照先をそちらに切り替えて下さい。
備考:コマンド
lipo
lipo(1) Mac OS X Manual Page
複数のアーキテクチャ向けのライブラリやアプリケーションバイナリを1つにまとめることができるツール。
[参考情報]
Undocumented Mac OS X:第13回 Universal Binary【後編】 (2/4) - ITmedia エンタープライズ
yebo blog: ユニバーサルバイナリ化するツール lipo
PlistBuddy(8) Mac OS X Manual Page
PlistBuddy
*.plist ファイルの値を設定したり取得したりできるコマンド。/usr/libexec/配下に存在する。
# CFBundleName 取得例 FRAMEWORK_NAME=$(/usr/libexec/PlistBuddy -c "Print CFBundleName" Info.plist)
PlistBuddy を使ってバージョン番号を自動的にインクリメントするアイディアも公開されている。
Incrementing Build Numbers in Xcode | Dave DeLong
備考
今回 PlistBuddy を使い、シェルスクリプトから Info.plist 内の情報を取り出してビルドに使用した。本来であればビルド設定の値(環境変数に設定される)を使うべきで、一般的な Xcodeプロジェクトではそのようになっている。Info.plist 自体にも環境変数が埋めこまれていてビルド時にこれが置換される。下記は Mac OS X 用 Framework テンプレートで生成された Info.plist。
${EXECUTABLE_NAME} など環境変数が使われているのがわかる。Info.plist 内の環境変数の置換はビルド時に行われている。以下は Mac OS X 用Framework をビルドしている時のメッセージ。
メッセージに表示されている builtin-infoPlistUtility は外部コマンドとして存在するものではなくて、Xcode内部の機能として存在するようだ。先頭にある "ProcessInfoPlistFile" はビルドのルール設定項目と思われるが見つからなかった(図にあるように CopyPlistFile というルールは設定項目にある)。
また今回使用しているターゲットタイプ(Shell Script)ではルールが編集できない。
仕方ないので
Info.plist ==(参照)==> 環境変数 シェルスクリプト ==(参照)==> 環境変数をやめて
Info.plist をオリジナル シェルスクリプト ==(参照)==> Info.plistとした。
Framework 参考情報
iOS向けの Framework に関しては Appleからドキュメントが提供されていない。この為、多くの人が自分たちで試行錯誤して Framework作成を試みている。そのおかげもあって Framework作成はだいたいすんなり行うことができた。情報提供してくれた方々にはこの場を借りてお礼を申し上げます。
iPhone アプリ開発で使える Framework の作り方 | KRAY Inc
Framework の作成方法が図入りで丁寧に解説されている。とても参考になった。
[Mac][iPhone][develop] iPhone OS用のほぼFrameworkの作り方 - Ni chicha, ni limona -平均から抜けられない僕-
ライブラリファイル(*.a)を lipo を使ってユニバーサル化する方法の紹介。
Mac♪Mac♪Mac♪ - 第11回 フレームワークを作成する
Mac OS X でフレームワークを作成する解説。
How to (almost) create your own iPhone OS framework
Making Your Own iPhone Frameworks @ Cocoanetics
“Making Your Own iPhone Frameworks”
Responses
Leave a Response