iOSにおける静的ライブラリ作成技法
この記事は VOYAGE GROUP Advent Canlendar 20日目の記事です。
こんにちは。@daybysayです。
私事ですが最近SDKエンジニアに転向したので、iOSにおける静的ライブラリ作成技法について勉強してまとめてみました。
iOSにおけるライブラリ、特にベンダーが提供するSDKと呼ばれている子たちは静的ライブラリ、あるいはそれをラップしたFramework形式で配布されることが多いです。
そこで、今回は静的ライブラリの作成方法とFramework化をするところまでを実装しました。
静的ライブラリ作成とFramework化を実現したプロジェクトはこちらになります。
静的ライブラリの作成
静的ライブラリを作成するには、まずCocoa Touch Static Library
テンプレートを用いてプロジェクトを作成します。
Xcode -> File -> New -> Project
から iOS -> Framework & Library -> Cocoa Touch Static Library
を選択します。
今回のプロジェクト(Framework)名はHOGEFuga
にしました。
プロジェクト作成時にHOGEFuga.h
とHOGEFufa.m
の2つのファイルが生成されると思います。
こちらは後ほど利用しますので、一旦無視してください。
利用者に提供する機能をもったクラスを実装する
今回はお試しなので、HOGEFugaService
というクラス名で、ログを吐くだけのメソッドを実装しました。
#import <Foundation/Foundation.h> @interface HOGEFugaService : NSObject + (void)show; @end
#import "HOGEFugaService.h" @implementation HOGEFugaService + (void)show { NSLog(@"hogehoge"); }
静的ライブラリをビルドする
あとはCmd + B
でビルドするだけで静的ライブラリが作成されます。とても簡単ですね。
出力先はここに書いてあります。
ちなみに静的ライブラリはlib{ターゲット名}.a
で出力されます。
ビルドする際の注意点は、内包するアーキテクチャを正しく指定することです。
現在のデフォルトでは armv64, armv7 が設定されているので、何もしないと下記のような出力結果になります。
$ file /Users/t-sei/Library/Developer/Xcode/DerivedData/HOGEFuga-fsxeplpuhlcgeseuqdiocmyhpvtp/Build/Products/Release-iphoneos/libHOGEFuga.a /Users/t-sei/Library/(略)/Release-iphoneos/libHOGEFuga.a: Mach-O universal binary with 2 architectures /Users/t-sei/Library/(略)/Release-iphoneos/libHOGEFuga.a (for architecture armv7): current ar archive random library /Users/t-sei/Library/(略)/Release-iphoneos/libHOGEFuga.a (for architecture arm64): current ar archive random library
Build Settings
で armv7s を追加することで、iPhone実機すべてのアーキテクチャに対応できるようになります。
設定後
file /Users/t-sei/Library/Developer/Xcode/DerivedData/HOGEFuga-fsxeplpuhlcgeseuqdiocmyhpvtp/Build/Products/Release-iphoneos/libHOGEFuga.a /Users/t-sei/Library/(略)/Release-iphoneos/libHOGEFuga.a: Mach-O universal binary with 3 architectures /Users/t-sei/Library/(略)/Release-iphoneos/libHOGEFuga.a (for architecture armv7): current ar archive random library /Users/t-sei/Library/(略)/Release-iphoneos/libHOGEFuga.a (for architecture arm64): current ar archive random library /Users/t-sei/Library/(略)/Release-iphoneos/libHOGEFuga.a (for architecture armv7s): current ar archive random library
また、iPhone Simulator用のアーキテクチャが内包された静的ライブラリはビルドターゲットをiPhone Simulatorにすることで出力可能です。
出力先は実機用静的ライブラリの隣のディレクトリに居ました。
実機用、SImulator用の2つのライブラリを作成後にlipo
コマンドを用いて1つにまとめるのが一般的のようです。
lipo -output libHOGEFuga.a -create /Users/t-sei/Library/(略)/Release-iphoneos/libHOGEFuga.a /Users/t-sei/Library/(略)/Release-iphonesimulator/libHOGEFuga.a
統合後
$ file libHOGEFuga.a libHOGEFuga.a: Mach-O universal binary with 5 architectures libHOGEFuga.a (for architecture armv7): current ar archive random library libHOGEFuga.a (for architecture armv7s): current ar archive random library libHOGEFuga.a (for architecture i386): current ar archive random library libHOGEFuga.a (for architecture x86_64): current ar archive random library libHOGEFuga.a (for architecture arm64): current ar archive random library
確認してみると、iPhone Simulator用の i386 と x86_64 が追加されているのがわかります。
これで1つのライブラリで実機でもiPhone Simulatorでも動くバイナリが出来上がりました。
調査した中では、XcodeのRun Script
を用いて上記の統合処理を行っているものが多かったのですが、個人的にRun Script
が得意ではないので、今回はMakefile
で実装しています。
XCODEBUILD?=$(shell which xcodebuild) TARGET_NAME?=HOGEFuga LIB_NAME:=lib${TARGET_NAME}.a PROJECT_NAME:=${TARGET_NAME}.xcodeproj BUILD_DIR:=./ CONFIGURATION?=Release libHOGEFuga.a: libHOGEFugaPhoneos.a libHOGEFugaPhonesimulator.a lipo -output $@ -create $^ rm -rf build/ Release-*/ libHOGEFugaPhoneos.a: ${XCODEBUILD} -target ${TARGET_NAME} -project ${PROJECT_NAME} -configuration ${CONFIGURATION} -sdk iphoneos -arch armv7 -arch armv7s -arch arm64 clean build ONLY_ACTIVE_ARCH=NO BUILD_DIR="${BUILD_DIR}" mv ${BUILD_DIR}/${CONFIGURATION}-iphoneos/${LIB_NAME} $@ libHOGEFugaPhonesimulator.a: ${XCODEBUILD} -target ${TARGET_NAME} -project ${PROJECT_NAME} -configuration ${CONFIGURATION} -sdk iphonesimulator -arch i386 -arch x86_64 clean build ONLY_ACTIVE_ARCH=NO BUILD_DIR="${BUILD_DIR}" mv ${BUILD_DIR}/${CONFIGURATION}-iphonesimulator/${LIB_NAME} $@
make libHOGEFuga.a
を叩くことでlibHOGEFugaPhoneos.a
とlibHOGEFugaPhonesimulator.a
がプロジェクトのルートに作成され、その2つのライブラリをlipo
で1つにまとめています。
Framework化する
静的ライブラリの形式でも利用は可能ですが、Framework化することで、画像や文字列ファイルやドキュメントなどのリソースをセットで管理できたり、複数のバージョンを内包できたりなど幾つかの利点があります。
詳しくはドキュメントに書いてあります。
ドキュメントにある通り、Framework自体はただのディレクトリ構成なのでここではディレクトリ作成が中心になります。
公式推奨のディレクトリ構成は下記になります。
MyFramework.framework/ MyFramework -> Versions/Current/MyFramework Resources -> Versions/Current/Resources Versions/ A/ MyFramework Resources/ English.lproj/ InfoPlist.strings Info.plist Current -> A
バージョニングですが、今回は必要性がなかったのでバージョニング用のディレクトリは作らず下記の構成にしました。
MyFramework.framework/ MyFramework Headers/ Resources/ Info.plist
Info.plistを作成する
必要性がいまいちわかっていないですが、必要らしいので作成しています。
くわしいことはこちらにあるっぽいので時間ができたら読んでみようと思います。
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> <dict> <key>CFBundleDevelopmentRegion</key> <string>English</string> <key>CFBundleExecutable</key> <string>HOGEFuga</string> <key>CFBundleGetInfoString</key> <string>HOGEFuga</string> <key>CFBundleIdentifier</key> <string>jp.co.hogehoge.HOGEFuga-framework</string> <key>CFBundleInfoDictionaryVersion</key> <string>6.0</string> <key>CFBundleName</key> <string>HOGEFuga</string> <key>CFBundlePackageType</key> <string>FMWK</string> <key>CFBundleShortVersionString</key> <string>1</string> <key>CFBundleSignature</key> <string>????</string> <key>CFBundleVersion</key> <string>1</string> <key>NSHumanReadableCopyright</key> <string>(c) 2015 Hoge All rights reserved.</string> </dict> </plist>
アンブレラヘッダを作成する
アンブレラヘッダは、公開すべきヘッダファイルをすべてインポートしたヘッダファイルであり、Framework名.h
として作るのが一般的です。
こちらの作成は必須ではないですが、利用側にとってメリットがあるので作っています。
今回は下記のようになります。
#import <Foundation/Foundation.h> #import <HOGEFuga/HOGEFugaService.h>
Frameworkにまとめる
最終的なプロジェクト構成はこんな感じになりました。
libHOGEFuga.a
やヘッダファイルなどをFramework形式に沿ったディレクトリの中に組み替える処理はまたMakefile
で実装しています。
TARGET_NAME?=HOGEFuga FW_DIR:=${TARGET_NAME}.framework FW_DIR_RES:=${FW_DIR}/Resoursec FW_DIR_HEAD:=${FW_DIR}/Headers FW_DIRS:=${FW_DIR_RES} ${FW_DIR_HEAD} HOGEFuga.framework: libHOGEFuga.a mkdir -p ${FW_DIRS} cp ${TARGET_NAME}/*.h ${FW_DIR_HEAD}/. cp ${TARGET_NAME}/Info.plist ${FW_DIR_RES}/. cp $^ ${FW_DIR}/${TARGET_NAME} libHOGEFuga.a: libHOGEFugaPhoneos.a libHOGEFugaPhonesimulator.a (略)
make HOGEFuga.framework
を叩くとHOGEFuga.framework
が作成されます。
Frameworkを利用する
利用している様子です。
利用する側では、アンブレラヘッダ(HOGEFuga.h
)をインポートしてHOGEFugaService
クラスのstaticメソッドをコールしています。
正しくログが吐かれているのが確認できました。
簡単でしたが静的ライブラリの作成からFramework化まで、以上となります。
最後に静的ライブラリ作成時に気をつけたほうが良いと思ったことを書いておきます。
静的ライブラリを作る際に注意すべきこと
静的ライブラリ作成時に限ったことではないですが、特に下記に注意すべきだと考えられます。
- 名前空間に気をつける
- privateAPIの利用を避ける
- ビルド時のパスに気をつける
名前空間に気をつける
Objective-Cは言語仕様として名前空間を持っていないため、クラス名にプレフィクスをつけることでシンボルの衝突を避ける必要があります。
滅多にないことですが、実際私も過去に2つのSDK内のシンボルが衝突してコンパイルできなくなった事があり、何もできずに頭を抱えたことがあります。
悲しみを生まないためにもシンボルの衝突を起こさない努力をしましょう。(あるいはSwiftを使う)
非公開APIの利用を避ける
非公開APIの利用は避けましょう。静的ライブラリ内で非公開API等を利用されてしまうと、利用者側のアプリが審査で落とされたり、そもそもSubmitできなくなったりします。 また過去に非公開APIの誤用によって、導入したアプリに脆弱性が埋め込まれるSDKが配布された事例がありました。このような事態を避けるためにも、非公開APIの利用は控えましょう。
ビルド時のパスに気をつける
strings
コマンドを用いることで、バイナリ内の文字列を取得する事が可能です。
静的ライブラリのビルド時にはビルドしたファイルのパスが残ってしまい恥ずかしい場合があるので、ビルドの際はパスに気をつけましょう。笑
strings HOGEFuga.framework/HOGEFuga | grep /User
/Users/t-sei/Development/misc/HOGEFuga/HOGEFuga
/Users/t-sei/Development/misc/HOGEFuga/HOGEFuga/HOGEFugaService.m
/Users/t-sei/Development/misc/HOGEFuga
こちらからは以上です。
明日は最近ツイートが減り気味の @co3k です。お楽しみに。
音楽再生時に、iPodアプリなどで再生している音楽を止めない方法
背景
AVAudioPlayerを使ってSEを再生する際に、Playerを読み込んだ時点で、iPodの音楽再生が止まってしまう。 BGMを流しているわけではないので、音楽再生を止めないようにしたい
実装
AVAudioPlayerを作る前に下記コードを実行する
// バックグラウンドでの音の再生を許可 [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryAmbient error:nil];
参考
ステータスバーの文字を白くする
ナビゲーションバーの色合い的に、白い文字のフオが映えそうなので色を変えたいという話が出たので調べてみた。
調査した実装方法
- Info.plistで設定する
- ソースコード内で実装を変える
- ビュー毎に変更できる
採用した実装方法
- Info.plistに記述して全体のスタイルを指定する
アプリ内で白と黒を使い分ける必要も無かったので。
実装
Info.plistに以下の項目を追加する
- "View controller-based status bar appearance"項目を追加して値をNOに設定する
- "Status bar style"項目を追加して、値を"Opaque black style"に設定する
調べてみるとStatus bar styleにUIStatusBarStyleLightContentを設定するという方法もあったけど、これだとちゃんと動きませんでした。
参考
Objective-Cにおけるバージョンチェック
今作ってるアプリにバージョンチェックを行う処理が実装されているが、上手く動いていないので、修正することに。
発生するバグ
例えば、ver1.0.3 や ver1.0.5 という値を 1.0とみなしてしまう
原因
取得したバージョン(文字列)をfloatにキャストしているため、2つ目のドット以下が無視されている。 以下バグってるコード
// dictionaryからversion情報を取得してfloatに float latestVersion = [[results objectForKey:@"version"] floatValue]; if ([[[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleShortVersionString"] floatValue] < latestVersion) { UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"最新版が公開されました" message:@"最新版へのアップデートをお願いします。" delegate:self cancelButtonTitle:@"アップデート" otherButtonTitles:nil]; [alert show]; }
対策
- 文字列の比較メソッドを使う
+ (BOOL)appVersionGreaterThanOr:(NSString *)version { NSString *currentVersion = [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleShortVersionString"]; NSComparisonResult result = [version compare:currentVersion options:NSNumericSearch]; return (result == NSOrderedAscending || result == NSOrderedSame) ? YES : NO; }
// dictionaryからversion情報を取得 NSString *latestVersion = [results objectForKey:@"version"]; if (![self appVersionGreaterThanOr:latestVersion]) { UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"最新版が公開されました" message:@"最新版へのアップデートをお願いします。" delegate:self cancelButtonTitle:@"アップデート" otherButtonTitles:nil]; [alert show]; }
参考
13日の金曜日に怪物と戦うために #vgadvent2013
VOYAGEGROUP 元android事業室の@DayBySayです。
このたび、android事業室はandroid事業本部になりました。( ゚Д゚ノノ☆パチパチパチパチ
ありがとうございます。
ということで、本日は13日の金曜日に襲ってくるであろう怪物と戦うために、準備するべきことを書いてみたいと思います。
※ この記事は VOYAGE GROUP エンジニアブログ : Advent Calendar 2013 の13日目の記事です。
13日の金曜日といえばこいつですね。
そう、皆さんご存知の
そう、JSONです。
きっとNOWいみなさんは何かの設定ファイルとしてJSONを使っちゃったりしちゃってるんじゃないかなと思います。僕は使ってませんが。
と、いうことで今日はキャンプに行った先に急にJSONに襲われても大丈夫なように、事前の準備をしてみようと思います!
1. JSONのことを知る
敵を知り己を知れば百戦危うからずと昔の偉い人が言っていたらしいです。
まずはJSONがどんな仕様なのか、今一度見てみましょう!
JSONを直接手で書く時によくやってしまう失敗として、
文字列を囲むのににシングルクォートを使ってしまったり、[ ] を用いて配列の表現をする際、カッコ内で{}で囲まずにKey Valueを並べちゃったりなどなど。
自分自身いくつか思いあたる点がありますが、そこは仕様をしっかりと確認することで無くせるミスだと思います。
ありがちなミスや、忘れがちな細かい仕様など、こちらのページにわかりやすくまとまっていて勉強になりました。
2. 武器を研ぎ澄ます
武器とは・・・エディタですね!
JSONと戦うときにエディタはよく出てくるとおもいますので(?)しっかりと対策をしておきましょう。
vim-jsonはvimにおけるJSON用のfiletypeプラグインです。
使うにはpathogenをセットアップし、bundle以下にvim-jsonをcloneしてくればオケオケオッケーです。
まずはpathogenをとってきます。
$ mkdir -p ~/.vim/autoload ~/.vim/bundle $ curl 'www.vim.org/scripts/download_script.php?src_id=19375' > ~/.vim/autoload/pathogen.vim
次に、.vimrcに以下の記述を加えます
call pathogen#infect()
そして、vim-jsonをbundle以下にクローンしてきます。
$ cd ~/.vim/bundle
$ git clone https://github.com/elzr/vim-json.git
vim-jsonを入れることで、この少し寂しい寂しい文字たちが
こんな感じですこし賑やかになります!
うーん、見た目的に楽しげ!
テンションを上げるためにも見た目を良くするのは大事ですね!
neat-json
neat-jsonは、vimのプラグインで、開いているファイル上のJSONを整形してくれるツールです。
NeoBundleを使っていれば、.vimrc に以下を追加することでインストールできます。
NeoBundle ‘5t111111/neat-json.vim’
NeoBundleについてはこちら
例えば次のようにvimでJSONを編集していたとします。
この状態で、 :NeatJson コマンドを実行すると・・・
こんな感じでインデントをつけてくれます。便利!(JSONが正しくない場合はコケます
何故か並びがアルファベット順になっていますが、この際気にしません。
そのへんは余力があるときにもうちょっと調べてみようと思います!
3. 飛び道具を使う
JSONをいじるときに助けてくれるコマンドラインツールはいくつか存在しており、それらも便利なので紹介させていただきます。
高速(らしい)なJSONパーサ
Macであれば、brewでインストールすることができます。
brew install yaji
いれると、json_verifyコマンドが使えるようになります
例えばこんな閉じカッコを忘れたJSONがあったとき
{"名前":"ジェイソン", "日付":"13日の金曜日","武器":[{"右手":"ナタ","左手":"斧"}]
json_verifyコマンドを使うことで、テキストの形式がJSONとして正しいかどうか教えてくれます。
$cat example.json |json_verify parse error: premature EOF (right here) ------^ JSON is invalid
今回はinvalidでした。
閉じカッコを付けてあげることで、validになりました。
{"名前":"ジェイソン", "日付":"13日の金曜日","武器":[{"右手":"ナタ","左手":"斧"}]}
$cat example.json |json_verify
JSON is valid
jsonpp
JSONを単純に整形して表示したいだけの場合jsonppが使えるかも知れません。
これもbrewでインストールできます。
brew install jsonpp
jsonppはjsonのインデントを整えてくれるコマンドです。
例えばこんなJSONを普通にcatすると
$cat example.json {"名前":"ジェイソン", "日付":"13日の金曜日","武器":[{"右手":"ナタ","左手":"斧"}]}
になりますが、jsonppに渡してあげると
$cat example.json | jsonpp { "name": "ジェイソン", "date": "13日の金曜日", "wepon": [ { "右手": "ナタ", "左手": "斧" } ], "helmet": "ホッケーマスク", "map": [ { "1": "キャンプ場", "2": "クリスタルレイク" } ] }
みやすくなりました。
jqを使うと更に高度なJSONいじりが可能になります。
jqもbrewでインストールできます。
brew install jq
jqはインデントを整えるだけではなく、データの絞り込みを行うことができます。
たとえば、次のようなJSONがあった時、配列の添字的にアクセスしてデタを抽出することが出来ます。
$cat victim.json | jq . [ { "name": "パメラ", "gender": "female" }, { "name": "アリス", "gender": "female" }, { "name": "ダイアナ", "gender": "female" }, { "name": "ポール", "gender": "male" }, { "name": "デューク", "gender": "male" } ]
$cat victim.json | jq .[0] { "name": "パメラ", "gender": "female" }
次のように書くことで、キーを指定した値の抽出が出来ます。
$cat victim.json | jq ".[].name" "パメラ" "アリス" "ダイアナ" "ポール" "デューク"
また、map selectを使うことで、条件を指定した絞り込みができます。
$cat victim.json | jq 'map(select(.["gender"] == "male"))' [ { "name": "ポール", "gender": "male" }, { "name": "デューク", "gender": "male" } ]
なんて便利!
もちろんcurlなどでapi叩いた結果をパイプで渡すこともできるので、レスポンスから直接弄りたいときにも使えるかもしれませんね。
てかjqいじってたらエラーも吐いてくれるしエラーの行も教えてくれるので、JSONをいじいじする際にこれだけあれば十分なのではという気がしてきました。。jqすごい
以上、JSONと戦うために役に立ちそうな道具を何点か紹介させていただきました。
これらを完備しておけば、キャンプ場で急にJSONに襲われた時も死なずにすむかもしれませんね!
次回は先輩エンジニア @lesamoureuses さんです。
僕より500倍は為になることを書いていただけるはずなので、皆様お楽しみに!
[hooks] gitのコミット時にイシュー番号を付け忘れるので・・
commit時にissue番号を付け忘れたら怒ってくれるスクリプトを作ってみました。
https://github.com/Takachon/issue_tukero
gitのリポジトリ(.git/hooks)にcommit-msg とprepare-commit-msgという名前のファイルを置くことで、
特定のタイミングで上記スクリプトが呼ばれて実行されます。
prepare-commit-msg はコミットを開始してエディタが起動するタイミング、
commit-msgはコミットを保存するタイミングですね。
http://git-scm.com/book/ja/Git-%E3%81%AE%E3%82%AB%E3%82%B9%E3%82%BF%E3%83%9E%E3%82%A4%E3%82%BA-Git-%E3%83%95%E3%83%83%E3%82%AF
また、今回はgitのtemplate機能も使ってみました。
上記プロジェクトをダウンロードして、makeを叩くと、gitのテンプレートが.gitconfigに設定されます。
template設定後は、git init した時に自動的に上記 prepare-commit-msg とcommit-msg ファイルが自動的に
hooksに配置されます。
以降、すべてのプロジェクトに適用されるので注意して下さい。
[Objective-c] [CoreAnimation] CAAnimationを連続して実行する時にbegintimeとdurationの計算がめんどう!
だったので、animationをaddしていけば勝手にdurationを計算してbeginTimeを設定してくれるクラスを作ってみました。
https://github.com/Takachon/VGASequentialAnimationGroup
いまは1つのアニメーションを連続して実行することしか出来ないんですが、
後々は複数のアニメーション(回転+移動+フェードイン)とかもうまいこと設定できるようにしたいです。
てか作ったのはいいけど、こんなん使わなくてもみんな普通にやってるのかな?
調べてもよくわからずだったので作ってしまった(´・ω・`)