Xcode 4.3でスタティックリンクライブラリィを扱う (その2

さて、今日はアプリケーションからスタティックライブラリをリンクする設定を行う手順をまとめる。
外部のライブラリィを自分のプロジェクトに組込む等、普段はこちらのほうが有用だろうか。

2.アプリケーションプロジェクトでスタティックリンクライブラリを組み込む

アプリケーションプロジェクトにスタティックリンクライブラリプロジェクトを追加する

まずは対象となっているアプリケーションのプロジェクトに、先日作ったスタティックリンクライブラリ用のプロジェクトを追加する。
方法は「Add Files to〜」で使用するライブラリのプロジェクト(xxxx.xcodeproj)を追加するだけだが、実はここが重要で、私はここで追加の方法の選択を間違っていたために失敗していたのである。


Xcode4.xの「Add Files to〜」によるプロジェクトへのファイル追加は対象となるディレクトリの扱いに関して二通りある。

  • Create groups for any added folders

追加したフォルダに対してXcode内で認識できる"グループ"を追加する。(黄色のフォルダとして描画される) ここで作成されるのは論理的なグループでありXcode内部でしか認識されない階層であり、作成後にはファイルシステムと同じように見えても同期している訳ではないので注意が必要である。

  • Create folder reference for any added folders

こちらは追加したフォルダに対してファイルシステム上の物理フォルダへの参照(リンク)を作成する。従ってOSのファイルシステムの構造がそのままでプロジェクトに追加される。(水色のフォルダとして描画される)

これらを用途によって使い分けなくてはならない訳だ。

スタティックリンクライブラリのプロジェクトを追加する場合、追加する対象はプロジェクトファイル(.xcodeproj)だが、追加時のフォルダの指定を後者の"Create folder reference for any added folders"にしなくてはならない。そうすることでXcodeは.xcodeprojを認識し、プロジェクトの構造がアプリケーションのプロジェクト内に展開される。

スタティックリンクライブラリのプロジェクトがアプリケーションプロジェクト下に読込まれているのが判るだろうか。

もう一度書くが、"Create groups for any added folders"では追加されるのはプロジェクトファイルのみであり、他のファイルが展開されない。"Create folder reference for any added folders"にしなくてはならない。

ターゲットのビルドフェーズでライブラリを組み込む

今までは"PROJECT"レベルの設定だったが、ここからは各ビルドTARGET毎の設定になる。
アプリケーションではスタティックライブラリのヘッダを参照するためリンク時にスタティックリンクライブラリを追加する必要がある。

  1. ボタンを押下するとダイアログが開き、上で追加したスタティックリンクライブラリプロジェクト中のライブラリの実体(xxx.a)が"Link Binary With Libraries"に見えているので追加する。


リンカの依存関係に必要なライブラリィとして追加されているのが判るだろうか。

ヘッダのパスを指定する (User Header Search Paths)

アプリケーションをエラー無しにコンパイルするには、スタティックリンクライブラリの作成時にpublicとして公開したヘッダファイルを参照できる必要がある。そのためにビルドセッティングの"User Header Search Paths"に環境変数$(BUILT_PRODUCTS_DIR)を設定する。(実際には$(BUILT_PRODUCTS_DIR)/$(TARGET_NAME)だが、上位のパスに/**が付加されてリカーシブサーチとなるようで、$(TARGET_NAME)は無くても探してくれるようだ)


ここまでの設定を間違いなく行えば、スタティックリンクライブラリを組み込んだ状態でアプリケーションプロジェクトのコンパイルが成功する。

はずなのだが、実際にはこのままコンパイルを行うと既存クラスを拡張するカテゴリをロードできずにコンパイルが失敗する。(成功しているように見える場合もあるが、実際には実行に失敗する。)

-ObjCのおまじないを追加する

以前にも書いたことがあるが、Objective-Cのリンカにはバグがあり、スタティックライブラリ中で定義されているカテゴリをロードすることができない。これを回避するためにはアプリケーション側のビルドセッティングパラメタ"Other Link Flags"にパラメタ"-ObjC"追加する必要がある。

Unrecognized Selector その後

さて、ではビルドして出力結果を見てみよう。

このようにアプリケーションのインストールディレクトリにアプリケーションバンドル(AppWithStaticLiv.app)と、スタティックライブラリ(LibtestStatic.a)と、公開設定されているヘッダファイル(TestStatic.h)がコピーされていれば成功だ。 おめでとう。

これで本当にスタティックライブラリを組み込んだiPhoneアプリケーション用のプロジェクトをビルドできるようになった。

それにしても面倒だ。
外部でビルドしたスタティックリンクライブラリを使うだけでこれだけの作業が必要とか、今回のように一度まとめることで簡単になるかなと思ったのだが、やはり面倒だった。 単にプロジェクトを追加するだけでコンパイル可能になるのが正しい姿だと思うのだが。

Xcode 4.3でスタティックリンクライブラリィを扱う (その1)

一度試して挫折するも、ちょっとしたことで上手くいったのでこれも備忘録としてメモ。

iOSはMacのようにローダブルバンドル※1は扱えないので、外部のライブラリィはソースコードとして取り込むか、静的(スタティック)なライブラリィとして組み込むしか無い。
フレームワーク」と呼ばれるパッケージを作ることもできるが、スタティックライブラリ以上に面倒そうなので、今回はスタティックなライブラリィをXcode4.3上で扱う手順をまとめる。(これは私の環境 Mac OS X Lion 10.7.3, Xcode 4.3.2上での記録であり、他の環境では試していない)

1.他から参照可能なスタティックライブラリィを作成する

Xcodeでプロジェクトを作成する


XcodeのNew Projectで開始するウィザードでプロジェクトを作成するが、その際に「Cooa Touch Static Library」を指定する必要がある。

ヘッダファイルを公開する


外部から参照するヘッダファイル(xxxx.h)はデフォルトではprojectスコープになっているので、File Inspectorで確認して外部から参照するヘッダは全てpublicスコープにする。

インストールディレクトリ(Installation Directory)を設定する

Xcode4のビルドパスは/Library/Developer/Xcode/DerivedData/であり※3、その下にプロダクト毎にハッシングされたディレクトリが作成されて必要なファイルがコピーされるのだが、ライブラリィプロジェクトのターゲットに対するインストールディレクトリ(Installation Directory)のデフォルトはなぜか/usr/local/libとなっており、これでは使えないので、環境変数$(BUILT_PRODUCTS_DIR)に変更する。

この設定によりビルドされたファイルは$(BUILT_PRODUCTS_DIR)下に出力される。

公開ヘッダ用のディレクトリ(Public Headers Folder Path)を設定する

外部に公開されるヘッダ用のディレクトリもインストールディレクトリ同様に、デフォルトは"usr/local/include"になっており、そのままでは使えないので、こちらは環境変数$(TARGET_NAME)に変更する。

この設定により、publicスコープに設定されているヘッダファイルが$(BUILT_PRODUCTS_DIR)/$(TARGET_NAME)下にコピーされて他のプロジェクトから参照できるようになる。


ライブラリ側はここまで。これで$(BUILT_PRODUCTS_DIR)/プラットホーム/produtの下にxxxx.aというスタティックリンク用のライブラリファイルが出来て、公開(public)に設定したヘッダファイルがコピーされていればOKだ。

作ったスタティックライブラリをリンクするアプリケーションプロジェクトの設定方法は明日にでも書こう。

※1:WindowsのDLLに相当する
※2:Xcode Build Setting Reference: Build Setting Reference
※3:Xcodeのプリファレンス->Locationで確認、変更できる

UITableView Tips

UITableViewを使用したプロトタイプをInterfaceBuilderで作っていたが、いくつかはViewController側にコードを書く必要があった。
参考にさせて頂いたサイトを紹介して、その点を備忘録としておく。

行を選択状態にしない

本来は属性"Show selection on touch"をチェックしないことで事足りるはずなのだが、何故かそうはならないので、tableView:cellForRowAtIndexPath:indexPathメソッドをオーバライドする。

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    UITableViewCell *cell = [super tableView:tableView cellForRowAtIndexPath:indexPath];
    if (cell) 
    {
        [cell setSelectionStyle:UITableViewCellSelectionStyleNone];
    }
    return cell;
}

参照サイト:iphone - a problem when disable Show selection on touch of UITableview? - Stack Overflow

固定行を設定する

UITableViewにはヘッダとなるヘッダビューを設定できるが、ヘッダと言う割にははそのままでは普通にスクロールしてしまう。スクロールしないようにするにはscrollViewDidScrollメソッドをオーバライドして同ビューの位置を固定する必要がある。

-(void)scrollViewDidScroll:(UIScrollView *)scrollView 
{
    UIView *headerView = [[self tableView] tableHeaderView];
    [headerView setFrame:CGRectMake(headerView.frame.origin.x
        , self.tableView.contentOffset.y
        , headerView.frame.size.width
        , headerView.frame.size.height)];
}

参照サイト:Storyboad+UITableView+StaticCellsでスクロールしない領域を作る方法 | 手作りプログラム

どちらも非常に重要なTipsであり、参考にさせて頂いたサイトには感謝。

設定等の標準画面を開くURLはiOS5.1では使えない。

アプリケーションからシステム標準の設定画面、例えばネットワーク設定画面等を呼び出したいことがままある。
Androidの場合、こんな時は"ACTION_WIFI_SETTINGS"インテント一発でシステム画面を呼び出すことができる。

iOSの場合はこんな芸当はできないだろうと思っていたのだが、先日調べてみた所iOS5までは以下の方法で出来るということが判った。

iOS5で全般(General)設定画面を呼び出す
[[UIApplication sharedApplication] openURL:[NSURL URLWithString:@"prefs:root=General"]];

これをiOS5.0シミュレータ上※で実行すると実際に全般設定の画面が表示される。

実行結果

おー、iOSもAndroid並とは行かないがIntentぽいことが出来るんじゃないか。これは素晴らしい。

ところがこれはiOS5.0までの話であり、iOS5.1上で全く同じコードを実行しても何も起きない。
試しにiOS5.1シミュレータのSafariで同URLを開いて見たのだが、やはり上手く行かない。(iOS5.0では開ける)

prefs:root:General

どうやらiOSレベルでこの方法がブロックされてしまっているようだが、この辺はiOS5.1β時代からの話らしい。
iOS 5.1 Disables Home Screen Settings Shortcuts

ブロックされている理由は恐らくセキュリティだろう。
このハックはこっそり通話を行う、メールを送信する等の悪用が出来てしまう訳で、内部に手を突っ込まれるのが嫌なのは判るが、システムの機能を共有したいという要求があるから皆使うわけで、代替えの方法は提供されていないのだろうか。


※iOSのシミュレータは非常に再現性が高く重宝する。Windows Phone7の時もその再現性には感心したが、Androidだけはいろいろと劣るのが残念だ。

FirstResponderを探せ

Windowsの世界では「フォーカスを持つオブジェクト」という言い方をするが、Macの世界では「最初に応答するオブジェクト」という意味のFirstResponderを探したい場合がある。

一番よく使われるのが現在処理対象になっているNSResponderの派生クラス(殆どのUI)にresignFirstResponderメッセージを送る」ことだろう。
NSResponderとして振る舞うことができるUIはresignFirstResponderメッセージを受けることにより、非アクティブ状態に移行する。
それ自体も重要だが、もっと重要なのはこのメッセージを受けたUIText、UITextView等の入力が可能なユーザインタフェースはその時点で開かれているソフトウェアキーボードを閉じる振る舞いである。

iOSのプログラミングで最初に困るのことの一つが「一度表示されたソフトウェアキーボードを閉じる術がない」※という問題だが、resignFirstResponderをUIに送ることでこの問題を解決することができるのだ。

コーディング方法はいろいろあるが、典型的なのは同メッセージをVireControllerで集約して、現在アクティブ状態のUIに対してメッセージを再送付する処理である。

HogeTableViewController.m
- (BOOL)resignFirstResponder
{
    UIView* responder = [[self tableView] findFirstResponder];
    if ( responder ) 
    {
        return [responder resignFirstResponder];
    }
    return NO;
}

findFirstResponderはUIView等のカテゴリで実装することで、インスタンスメソッドのように使えるだろう。

UIView+UIUtil.m
#import "UIView+UIUtil.h"
@implementation UIView (UIUtil)
- (UIView *)findFirstResponder
{
    if ([self isFirstResponder]) 
    {
        return self;
    }
    for (UIView *subView in [self subviews]) 
    {
        if ([subView isFirstResponder])
        {
            return subView;
        }
        if ([subView findFirstResponder])
        {
            return [subView findFirstResponder];
        }
    }
    return nil;
}
@end

※かくいう私もそうだ。

Objective-Cの好きなところ

私は評論家ではなく開発者なので、Objective-Cの良い所(というか好きな所)も書いておこうと思う。

オブジェクト指向であること

Objective-Cなのだから当たり前なのだが、継承、委譲、カプセル化ポリモーフィズム等、他のオブジェクト指向言語と同様にオブジェクト指向の恩恵を享受できるのは有り難い。
C言語でもオブジェクト指向はある程度実装できるが、やはり言語でそれを規定されているのとは全然違う。
特に、id型という全てのオブジェクト型を格納できる型(JavaC#のObject型に相当する)の存在や、ヌルパターン(nilにメッセージを送っても例外やエラーにならない)はシンプルで動的なプログラミングを可能にしている。

C言語であること

昨日とは逆説になるが、C言語がベースになっているということはもちろん悪いことばかりではない。
今も使われている普遍的なソフトウェアのかなりの部分はCで書かれているかCからポーティングされており、膨大なソースコードObjective-Cのプラットホームに取込むことが出来るのは非常に有り難い。
あるソフトウェアをObjective-Cにポーティングする場合でもそのままダイレクトにポーティングするよりも、Cで書かれた部分のそのまま使い、フロントエンドだけでObjective-Cで書く方法の方が書くコードが少なくて済む。

また、Objective-C特有のセレクタやブロックなどはC言語の関数ポインタの機能を利用しており、C言語の良い所を活用している。

拡張性、柔軟性が高いこと

他の言語では忌避すべきことだったり、エラーを発生させることがプログラマの責任において可能になっており、拡張性や柔軟性が非常に高い。(まるでRubyでコードを書いているような気持ちになることがある)
特に、他のオブジェクト型の実装を継承無しに拡張することができるカテゴリ(無名や、オブジェクトのメソッドの実体を置換える(Method Swizzling)等は危険だが非常に強力な機能である。

まだまだたくさんの良い所があるのだが、取りあえずこの辺で。

現在において100点満点の言語というのは存在せず、どんなプログラミング言語でもそれぞれ長所と短所が存在する訳で、その中で長所を生かし短所を補いながら使うことが非常に重要だ。

Objective-Cの一寸残念な所

2ヶ月程度Objective-Cを勉強してきたが、少しずつその特徴が見えてきた。
カテゴリやプロトコル等、素晴らしい点がたくさんあるのだが、同時にいくつかの残念だとと思う点も見えてきた。

C言語であること

Objective-CJavaC#等のようにCのシンタクスをベースにしているものの、設計を1から行った言語などとは違いあくまでC言語が下地になっている。
objc-class.mやobjc-runtime.mのソースコードを見れば判るが、低レベルな処理はC言語アセンブラで書かれており、そこの部分を調べるにはC言語の基礎知識が必要であり、敷居は低くない。

スカラ型とオブジェクト型の相互変換

初期のJavaでも問題になったが、Cで元々サポートしているスカラ型とObjective-Cで扱うオブジェクト型は相互の代入互換性は無いため、これらのデータを交換するには相互変換が必要になる。(相互変換を暗黙的に行うボクシング/アンボクシングは一部の機能しか実装されていない)

また、オブジェクト型といってもその実態は構造体へのポインタであり、型の宣言でもポインタ型であることを明示的に記述しなくてはならない(型の末尾又は変数の頭に'*'を付加する必要がある。
この辺、同じハイブリッド型の言語であるDelphiはオブジェクトがポインタ型であることを上手く隠蔽しており、殆どポインタを意識する必要が無いのと対照的だ。

中途半端なランタイム情報

Objective-Cは「ダイナミック」なC言語と呼ばれるが、その大きな理由の一つはランタイムにクラス、メッセージ、プロパティなどのメタデータを取得できることである。
具体的にはランタイムAPIを使用して、JavaC#のリフレクションのようにクラスの情報からそのフィールド、プロパティ、メッセージ(メソッド)を取得できるのだが、この機能が私には中途半端に感じる。

一番困るのが、メッセージのシグネチャ、パラメタの静的な型がランタイムには判らないことである。 そんな馬鹿なと思うだろう?

- (void)hogeMessage:(id)target forName:(NSString *)name forVale:(MyValue *)value
{
  〜
}

このようなメソッド(メッセージ)があったとして、それぞれのパラメタ(forName:forValue)の型を調べるコードは以下のようになる。

NSMethodSignature* sig = [object methodSignatureForSelector:@selector(HogeMessage:forName:forValue)];
unsigned int argCount = [sig numberOfArguments];
for ( int index = 2 ; index < argCount; index++)
{
    // argTypeにパラメタの型がエンコードされた文字がセットされる
    const char* argType = [sig getArgumentTypeAtIndex:index];
}

ここでargTypeに戻されるのは型をエンコードした文字であり、それは以下のように規定されている。
Objective-C Runtime Programming Guide: Type Encodings

c char
i int
s short
l long
q long long
C unsigned char
I unsigned int
S unsigned short
L unsigned long
Q unsigned long long
f float
d double
B C++のbool
v void
* 文字列(char*)
@ オブジェクト (静的に型定義されているまたはidとして型定義されているもの)
# クラスオブジェクト(Class)
: メソッドセレクタ(SEL)
[配列型] 配列
{名前=型...} 構造体
(型...) 共用体
bnum num ビットフィールド
^型 型へのポインタ
? 不明な型(関数ポインタ)

スカラ型はエンコードされた文字から一意に型が決まるので良いのだが、問題はオブジェクト型である。

もう一度、オブジェクト型のエンコード文字を見てみよう。

@ オブジェクト (静的に型定義されているまたはidとして型定義されているもの)

つまりはどんな型定義がされていても全てのオブジェクト型は"@"一文字の情報しか持たない、ということである。

これは上記hogeMessageメッセージのシグネチャで記述されているパラメタ:forNameと:forValueそれぞれNSString*とMyValue*という静的な型で宣言されているが、ランタイム情報としてパラメタの型を調べて、NSString*型やMyValue*型かどうか確認する術が無いということである。
シグネチャに書かれているパラメタの型はコンパイル時の型チェックにしか使われていないようだ。(まるでイレイジャのようだ)

なお、同じようにエンコードされた文字で型を表しているプロパティは静的な型のランタイム情報を持っている。

プロパティのエンコードされた型情報の例
 NSString*型のプロパティ -> T@"NSString"
 MyValue*型のプロパティ -> "T@"MyValue"

これによって、プロパティの正しい型を特定できる。

どうしてメソッドもプロパティと同じ仕様にしなかったのだろうな。