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 を選択します。 f:id:DayBySay:20151220032715p:plain

今回のプロジェクト(Framework)名はHOGEFugaにしました。

プロジェクト作成時にHOGEFuga.hHOGEFufa.mの2つのファイルが生成されると思います。 こちらは後ほど利用しますので、一旦無視してください。

利用者に提供する機能をもったクラスを実装する

今回はお試しなので、HOGEFugaServiceというクラス名で、ログを吐くだけのメソッドを実装しました。

#import <Foundation/Foundation.h>

@interface HOGEFugaService : NSObject

+ (void)show;

@end
#import "HOGEFugaService.h"

@implementation HOGEFugaService

+ (void)show {
    NSLog(@"hogehoge");
}

静的ライブラリをビルドする

あとはCmd + Bでビルドするだけで静的ライブラリが作成されます。とても簡単ですね。

出力先はここに書いてあります。 f:id:DayBySay:20151220033103p:plain

ちなみに静的ライブラリは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実機すべてのアーキテクチャに対応できるようになります。 f:id:DayBySay:20151220033149p:plain

設定後

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にすることで出力可能です。 f:id:DayBySay:20151220043434p:plain

出力先は実機用静的ライブラリの隣のディレクトリに居ました。 f:id:DayBySay:20151220043546p:plain

実機用、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用の i386x86_64 が追加されているのがわかります。

これで1つのライブラリで実機でもiPhone Simulatorでも動くバイナリが出来上がりました。

調査した中では、XcodeRun 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.alibHOGEFugaPhonesimulator.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にまとめる

最終的なプロジェクト構成はこんな感じになりました。 f:id:DayBySay:20151220041646p:plain

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が作成されます。

f:id:DayBySay:20151220034459p:plain

Frameworkを利用する

利用している様子です。

f:id:DayBySay:20151220034640p:plain

利用する側では、アンブレラヘッダ(HOGEFuga.h)をインポートしてHOGEFugaServiceクラスのstaticメソッドをコールしています。 正しくログが吐かれているのが確認できました。

簡単でしたが静的ライブラリの作成からFramework化まで、以上となります。

最後に静的ライブラリ作成時に気をつけたほうが良いと思ったことを書いておきます。

静的ライブラリを作る際に注意すべきこと

静的ライブラリ作成時に限ったことではないですが、特に下記に注意すべきだと考えられます。

  1. 名前空間に気をつける
  2. privateAPIの利用を避ける
  3. ビルド時のパスに気をつける

名前空間に気をつける

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];

参考

ステータスバーの文字を白くする

ナビゲーションバーの色合い的に、白い文字のフオが映えそうなので色を変えたいという話が出たので調べてみた。

調査した実装方法

  1. Info.plistで設定する
  2. ソースコード内で実装を変える
  3. ビュー毎に変更できる

採用した実装方法

  • Info.plistに記述して全体のスタイルを指定する

アプリ内で白と黒を使い分ける必要も無かったので。

実装

Info.plistに以下の項目を追加する

  1. "View controller-based status bar appearance"項目を追加して値をNOに設定する
  2. "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];
}

対策

  1. 文字列の比較メソッドを使う
+ (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日の金曜日といえばこいつですね。

そう、皆さんご存知の

f:id:DayBySay:20081208180741j:plain


そう、JSONです。
きっとNOWいみなさんは何かの設定ファイルとしてJSONを使っちゃったりしちゃってるんじゃないかなと思います。僕は使ってませんが。


と、いうことで今日はキャンプに行った先に急にJSONに襲われても大丈夫なように、事前の準備をしてみようと思います!

1. JSONのことを知る

敵を知り己を知れば百戦危うからずと昔の偉い人が言っていたらしいです。

まずはJSONがどんな仕様なのか、今一度見てみましょう!

http://www.json.org/JSON


JSONを直接手で書く時によくやってしまう失敗として、

文字列を囲むのににシングルクォートを使ってしまったり、[ ] を用いて配列の表現をする際、カッコ内で{}で囲まずにKey Valueを並べちゃったりなどなど。

自分自身いくつか思いあたる点がありますが、そこは仕様をしっかりと確認することで無くせるミスだと思います。

ありがちなミスや、忘れがちな細かい仕様など、こちらのページにわかりやすくまとまっていて勉強になりました。

もう一度、ちゃんとJSON入門


2. 武器を研ぎ澄ます

武器とは・・・エディタですね!

JSONと戦うときにエディタはよく出てくるとおもいますので(?)しっかりと対策をしておきましょう。


vim-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を入れることで、この少し寂しい寂しい文字たちが
f:id:DayBySay:20131213020517p:plain

こんな感じですこし賑やかになります!
f:id:DayBySay:20131213020500p:plain

うーん、見た目的に楽しげ!

テンションを上げるためにも見た目を良くするのは大事ですね!


neat-json
neat-jsonは、vimのプラグインで、開いているファイル上のJSONを整形してくれるツールです。

NeoBundleを使っていれば、.vimrc に以下を追加することでインストールできます。

 NeoBundle ‘5t111111/neat-json.vim’

NeoBundleについてはこちら


例えば次のようにvimでJSONを編集していたとします。

f:id:DayBySay:20131213020517p:plain

この状態で、 :NeatJson コマンドを実行すると・・・

f:id:DayBySay:20131213020650p:plain

こんな感じでインデントをつけてくれます。便利!(JSONが正しくない場合はコケます

何故か並びがアルファベット順になっていますが、この際気にしません。
そのへんは余力があるときにもうちょっと調べてみようと思います!

3. 飛び道具を使う

JSONをいじるときに助けてくれるコマンドラインツールはいくつか存在しており、それらも便利なので紹介させていただきます。


yajl

高速(らしい)な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

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つのアニメーションを連続して実行することしか出来ないんですが、
後々は複数のアニメーション(回転+移動+フェードイン)とかもうまいこと設定できるようにしたいです。

てか作ったのはいいけど、こんなん使わなくてもみんな普通にやってるのかな?
調べてもよくわからずだったので作ってしまった(´・ω・`)