bricklifeのはてなブログ

気軽にブログ書くよ!

WWDC 2012の基調講演で出てきたiOS 6とSDKのマイナーな新要素まとめ

今年こそは参加したかったのにチケットが買えずに断念した、WWDC 2012の基調講演のビデオが公開されました。

Apple - Apple Events - Apple Special Event June 2012

後半に出てくる2枚のまとめスライドに書かれているiOS 6とSDKの新要素を列挙してみます。API Diffを見る前にざっと概略をつかむのによいと思います*1。公開情報のわりにはメディアなどにあまり取り上げられていないものも結構ありますよ。

iOS 6

Apple - Apple Events - Apple Special Event June 2012

  • Features for China
  • Game Center challenges
  • Faster Safari JavaScript
  • Location-based reminders for iPad
  • Game Center friends from Facebook
  • Autocorrection for every keyboard
  • Alarm with song
  • Global network proxy for HTTP
  • Manual reorder of reminders
  • Improved privacy controls
  • Lost Mode
  • Bluetooth MAP support
    • MAPは「Message Access Profile」の略で、デバイス同士でデバイス間でメッセージやりとりするためのプロファイル
  • Kernel ASLR
  • Manual location entry for reminders
  • Custom vibrations for alerts
  • VoiceOver improvements
  • App in Safari search results
  • Personal dictionary in iCloud
    • iCloudでユーザ辞書を共有できる!?
  • Redesigned Stores
  • Per account signatures in Mail
    • アカウントごとの署名設定。結構うれしい
  • IPv6 support for Wi-Fi and LTE
  • HDR improvements
  • French, German and Spanish dictionaries
  • Word highlights for speak selection
  • Search all fields in Contacts
  • New iPad Clock app
    • iPadにも時計アプリが!
  • Improved keyboard layouts
  • Made for iPhone hearing aids
    • 補聴器機能が公式に出るのかな?

SDK

Apple - Apple Events - Apple Special Event June 2012

  • Audio and video sampling during playback
    • 音楽やビデオを再生しながらサンプリングできるようになるのかな?
  • Pass Kit
  • Rich text on labels, fields and text views
    • あらゆるテキストコントロール上でリッチテキストが使えるように! うれしい!
  • VoiceOver gestures
  • Control camera focus and exposure
    • フォーカスや露出を自由にコントロールできるようになるのかな?
  • Remote web inspector
    • インスペクターでのリモートデバッグを公式サポートの模様
  • CSS filters
  • Action Sheet
    • なんだろ。アクションシートがもっと使いやすくなるといいなー
  • Web Audio API
    • 楽しそう
  • Crossfade with CSS animation
  • Game Center in-app experience
    • よくわかんないけどもっとアプリに融合できるってことかしら
  • Reminders
  • Video stabilization
  • Game groups
  • Bluetooth MAP support
    • 大事なことなので2回(ry SPPは一生サポートされないんだろうか…
  • Transit apps
    • わからん。乗り換え案内的な機能が使えるようになるとか?
  • In-app Bluetooth pairing
    • アプリ内でBluetoothのペアリングを管理できる!?
  • Face detection API
  • Inter-app audio
    • 意味がわからないけど、気になる
  • Frame drop data
    • 映像処理系? 気になるけどまったく意味不明
  • Map Kit
  • Auto layout
    • リフロー的な機能?
  • State preservation
    • メモリから解放されてもアプリの状態を復活できる機能だとうれしいな
  • Pull to refresh on Table views
    • ひっぱって更新も標準サポート
  • In-app purchase hosted content
    • こっちにも書いてあるけど、Appleのサーバで課金コンテンツのファイルを管理してくれるようになる模様。これはうれしい
  • Read and write image metadata
  • Collection views
    • ビューの管理が楽になる系?
  • In-app content purchases
    • これもこっちに書いてあるけど、iTunes上のあらゆるコンテンツをアプリ内で購入できるようになるそうな
  • Multi-route audio
    • むむむ? Inter-app audioと合わせて要調査

iOS 6とそのSDKに関しては以上ですが、OS Xに関してのまとめスライドもあるので気になる人はどうぞ!

*1:実はいまだに開発機がSnow Leopardなので、まだ新しいXcodeも入れていないしAPI Diffも見ていない…

わかっているようでわかっていなかったBlocksの仕組み

iOS 4から使えるようになったBlocksは、余計なことを気にせず気軽に使えるとてもよい拡張機能で、僕も便利にガンガン使わせてもらっています。

ただ、表向きは親切なのですが、裏側の実装は変態的なことになっており、それを理解するまで疑問に思うことや不安を感じることが多々ありました。

というわけで、自分の復習もかねて、Blocksに関して色々調べた結果をここにまとめておきます。随時追記予定&つっこみ歓迎です。

Blockの正体は構造体

下記のようにBlock変数 myblock を定義したあとでブレークさせると、XcodeのVariables Viewでその中身を見ることができます。

Xcode

「myblock = (struct __block_literal_1 *) 0xbfffd490」ということなので、myblock は __block_literal_1 という型の構造体のポインタ変数で、その実体はスタックに確保されているらしいことがわかります。

注目は、構造体のメンバとして __FuncPtr があることです。名前から推測できるように、これは関数ポインタを格納するメンバ変数です。

実はこれ、

void (^myblock)() = ^{
    NSLog(@"%d", i);
};

というふうにBlock変数を定義すると、コンパイル時に

{
    NSLog(@"%d", i);
}

の部分が関数として自動的に定義されて、その関数へのポインタが __FuncPtr に格納される、という仕組みになっているのです。

「えー、じゃあこの関数で使っている i はどこで定義されるの?」という疑問がわきますが、さきほど見た __block_literal_1 型の構造体のメンバとして、一番下に i が定義されているのに気づいたでしょうか? 実はこの関数が定義されるときに、構造体のメンバとして持っている i が使われるようなコードに置き換わるのです。

ということは、この構造体の型 __block_literal_1 は、__FuncPtr が指す関数内で使っている変数によって、そのメンバ構成が変わらなくてはいけないことになるわけですが、まさにそのとおり。コード内でBlockを定義すると、その正体である構造体の型も必要に応じて自動的に定義されます。定義が増えるたびに、__block_literal_2、__block_literal_3 と型名の最後の数字が増えていくわけです。

そして、このBlockを実行するために myblock(); と書いた部分は、myblock が持つ FuncPtr で示された関数に myblock 自身を渡して実行されるコードに置き換わります。

こんな感じで、たった数行のコードから色んなものが自動に追加されたり置き換わったりすることで、Blocksの便利な機能が実現されているのです。

Blockはその定義によって配置されるメモリの位置が違う

iOS4プログラミングブック」などのBlocksの解説を見ると、新しく定義したBlockが配置されるメモリの位置は、ちょっと特殊なルールで決まるそうです。

Blockの内容 メモリの位置
Block外のローカルな自動変数を使っているBlock スタック
Block外のローカルな自動変数を使っていないBlock .dataセクション

スタックにあるBlockをヒープにコピーする Block_copy() を使った場合はこうなります。

コピー元のBlockの内容 コピー後のメモリの位置
Block外のローカルな自動変数を使っているBlock ヒープ
それ以外のBlock 元の場所と同じ(.dataセクション or ヒープ)

本当にこうなるか実際に試してみます。

int stack_int = 1;
static int global_int = 1;
NSLog(@"stack_int:\t%d : %p", stack_int, &stack_int);
NSLog(@"global_int:\t%d : %p", global_int, &global_int);

void (^stack_block)() = ^{
    NSLog(@"stack_int:\t%d : %p", stack_int, &stack_int);
};
NSLog(@"stack_block:\t%@", stack_block);
   
void (^global_block)() = ^{
    NSLog(@"global_int:\t%d : %p", global_int, &global_int);
};
NSLog(@"global_block:\t%@", global_block);

void (^copy_stack_block)() = Block_copy(stack_block);
NSLog(@"copy_stack_block:\t%@", copy_stack_block);

void (^copy_global_block)() = Block_copy(global_block);
NSLog(@"copy_global_block:\t%@", copy_global_block);
   
void (^copy_copy_stack_block)() = Block_copy(copy_stack_block);
NSLog(@"copy_copy_stack_block:\t%@", copy_copy_stack_block);
   
void (^copy_copy_global_block)() = Block_copy(copy_global_block);
NSLog(@"copy_copy_global_block:\t%@", copy_copy_global_block);

Block_release(copy_stack_block);
Block_release(copy_global_block);
Block_release(copy_copy_stack_block);
Block_release(copy_copy_global_block);

Blockはオブジェクトとして振る舞うので、NSLogで %@ とするとその中身を簡単に見ることができます。出力結果はこんな感じ。

2012-03-28 13:53:26.235 xxx[3781:207] stack_int:	1 : 0xbfffd4b0
2012-03-28 13:53:26.237 xxx[3781:207] global_int:	1 : 0x4660
2012-03-28 13:53:26.238 xxx[3781:207] stack_block:	<__NSStackBlock__: 0xbfffd490>
2012-03-28 13:53:26.239 xxx[3781:207] global_block:	<__NSGlobalBlock__: 0x4698>
2012-03-28 13:53:26.240 xxx[3781:207] copy_stack_block:	<__NSMallocBlock__: 0x6804900>
2012-03-28 13:53:26.241 xxx[3781:207] copy_global_block:	<__NSGlobalBlock__: 0x4698>
2012-03-28 13:53:26.242 xxx[3781:207] copy_copy_stack_block:	<__NSMallocBlock__: 0x6804900>
2012-03-28 13:53:26.242 xxx[3781:207] copy_copy_global_block:	<__NSGlobalBlock__: 0x4698>

まさに冒頭の表に書かれている結果になりました。

__NSStackBlock__ を Block_copy() すると __NSMallocBlock__ になり、どちらもそれっぽいアドレスに配置されています。また、__NSMallocBlock__ と __NSGlobalBlock__ は何回 Block_copy() しても、配置される領域だけでなくアドレスすら変わらないことがわかります。

実際には、__NSMallocBlock__ を再度 Block_copy() すると、新たにコピーされずに参照カウントが増える形になります。NSObject の retain のような動作をするのですが、どちらにしろ Block_copy() した数だけ Block_release() しないとメモリリークします。

一方、__NSGlobalBlock__ の場合は参照カウントすら増やしません。おそらく構造体のアドレスをコピーしているだけではないでしょうか。Block_release() も何にも処理をしていないっぽいです。なので、もし Block_copy() 後に Block_release() しなくても、なんとメモリリークしません!

でも両者をいちいち区別するのは面倒なので、確保しておきたいBlockは Block_copy() して、使わなくなったら Block_release() するのがよいでしょう。

キャプチャの正体は構造体メンバへのコピー

では、以下のようなコードを Block_release() の前に追加して、定義したBlock達を実行してみます。

stack_int++;
stack_block();

global_int++;
global_block();

stack_int++;
copy_stack_block();

global_int++;
copy_global_block();

stack_int++;
copy_copy_stack_block();
   
global_int++;
copy_copy_global_block();

実行結果。最初に記述した部分も含みます。

2012-03-28 13:59:10.212 xxx[3811:207] stack_int:	1 : 0xbfffd4b0
2012-03-28 13:59:10.214 xxx[3811:207] global_int:	1 : 0x4660
2012-03-28 13:59:10.215 xxx[3811:207] stack_block:	<__NSStackBlock__: 0xbfffd490>
2012-03-28 13:59:10.216 xxx[3811:207] global_block:	<__NSGlobalBlock__: 0x4698>
2012-03-28 13:59:10.217 xxx[3811:207] copy_stack_block:	<__NSMallocBlock__: 0x68390e0>
2012-03-28 13:59:10.218 xxx[3811:207] copy_global_block:	<__NSGlobalBlock__: 0x4698>
2012-03-28 13:59:10.265 xxx[3811:207] copy_copy_stack_block:	<__NSMallocBlock__: 0x68390e0>
2012-03-28 13:59:10.265 xxx[3811:207] copy_copy_global_block:	<__NSGlobalBlock__: 0x4698>
2012-03-28 13:59:10.266 xxx[3811:207] stack_int:	1 : 0xbfffd4a4
2012-03-28 13:59:10.267 xxx[3811:207] global_int:	2 : 0x4660
2012-03-28 13:59:10.272 xxx[3811:207] stack_int:	1 : 0x68390f4
2012-03-28 13:59:10.273 xxx[3811:207] global_int:	3 : 0x4660
2012-03-28 13:59:10.274 xxx[3811:207] stack_int:	1 : 0x68390f4
2012-03-28 13:59:10.275 xxx[3811:207] global_int:	4 : 0x4660

ローカルな自動変数である stack_int をいくらインクリメントしても、Blockで出力されるのはBlockを定義したときの値になっている、というのはBlocksの「キャプチャ」と呼ばれる機能の一例です。それも大事なのですが、ここでは変数のアドレスに注目します。

最初に定義した stack_int のアドレスと stack_block() で出力される stack_int のアドレスは、同じスタック内でも違う位置になっています。これは、前述のように stack_block が示す構造体のメンバのほうの stack_int が使われている、という事実を表しています。その値は stack_block を定義したときにローカル変数 stack_int からコピーされるため、その後いくらローカル変数を変更しても変化しない、というわけです。

また、stack_block を Block_copy() した copy_stack_block の実体はヒープ上に配置されますので、copy_stack_block() で出力される stack_int のアドレスもヒープ上になっています。そして、以後何回 Block_copy() してもそのアドレスは変わっていません。

一方、同じローカル変数でもstaticな変数である global_int はどこでもアドレスが変わらず、値の変化にも追従していることがわかります。Block内で使おうとしている変数がstaticである場合、プログラム実行中はメモリに確保され続けるため、構造体のメンバもポインタ変数になってそこに元の変数のアドレスがコピーされるようです。関数内のコードもポインタ変数の操作に置き換わります。なので、どこで値を変更しても反映されるんですね。

__block修飾子の正体も構造体

Blockでキャプチャしたローカル変数をBlock内で変更できるようにするには、__block修飾子を使います。__block修飾子がついた変数はBlock内外の変更に追従します。

__block int i = 1;

void (^myblock)() = ^{
    NSLog(@"%d : %p (myblock)", i, &i);
};

i++;
NSLog(@"%d : %p", i, &i);
myblock();

void (^copy_myblock)() = Block_copy(myblock);

i++;
NSLog(@"%d : %p", i, &i);
copy_myblock();

i++;
NSLog(@"%d : %p", i, &i);
myblock();

Block_release(copy_myblock);

このコードの結果はちょっと面白いですよ。

2012-03-29 01:00:33.235 xxx[5613:207] 2 : 0xbfffd560
2012-03-29 01:00:33.239 xxx[5613:207] 2 : 0xbfffd560 (myblock)
2012-03-29 01:00:33.257 xxx[5613:207] 3 : 0x6865000
2012-03-29 01:00:33.258 xxx[5613:207] 3 : 0x6865000 (myblock)
2012-03-29 01:00:33.260 xxx[5613:207] 4 : 0x6865000
2012-03-29 01:00:33.262 xxx[5613:207] 4 : 0x6865000 (myblock)

i がちゃんとインクリメントされているのはいいとして、なんと、ローカル変数 i と myblock の i が同じアドレスを指しています。さっきの説明と違いますね。

また、驚くべきことに、そのアドレスが途中からヒープ上になってしまっています! int i として定義したローカルな自動変数のアドレスが関数内で変わるなんて、普通ではちょっと考えられません。

さらに、スタック上にあるはずの myblock の最後の実行結果も、同じヒープ上のアドレスを表示しています。わけがわからない…。

実はこの現象は、__block修飾子をつけた変数の宣言部分が、Blockとはまた別の構造体の宣言に置き換わることによって発生するのです。__block修飾子がついた変数の正体も構造体なのです。

ということは、Block変数のときように、XcodeのVariables Viewでその実体と値の変化を見ることができそうです。アドレスが変化しているのは Block_copy() のせいっぽいので、Block_copy() の行でブレークしてみます。

Blocks 3-1

んー、i は (int) のままですねぇ…。

ここで i のアドレスも監視したいので、Variables Viewの中で右クリックして「Add Expression...」を選択し、「&i」と入力します。

Blocks 3-2

この時点では、アドレスはスタック上のままのようです。

Block 3-3

では、この状態でStep Overして Block_copy() を実行させます。

Blocks 3-4

おお、i は相変わらず (int) のままですが、&i の値がヒープ上になりました! なんだこれ!

どうやらXcode(というかgdb)は、__block修飾子がついた変数をデバッグしやすいように元の形のまま表示してくれるようで、こうやっても正体を見せてくれないんですね。うむー。

さて、このままだと__block修飾子の正体がわからないのですが、実は myblock や copy_myblock の構造体のメンバにその正体を見ることができます。

Blocks 3-5

ローカル変数のキャプチャの仕組みと同様、Blockの構造体に i というメンバが追加されており、それが構造体のポインタ変数として定義されていることがわかります。この構造体が__block修飾子をつけられた変数の正体です。

この構造体の中にはさらに別の i が定義されており、どうやら僕らはその値やアドレスを見ていたようです。

また、myblock が持つ構造体 i のメンバ __forwarding の値が、copy_myblock のほうの i が指しているアドレスになっているのに気づきます。この仕組みをここで説明するのは大変なので省きますが、この __forwarding が指すアドレスが必要に応じて切り替わることで、i がメモリ上のどこにあっても気にせず使えるようになっているのです。

__block修飾子の詳しい話は、「iOS4プログラミングブック」でBlocksやGCDの章を執筆された坂本一樹 (@splhack) 氏のブログに書かれていますので、ぜひ一度ご覧下さい。今回書いていない「clang -rewrite-objc」での解析などの話もあって非常にためになります。

Blockを含むBlockをBlock_copy()するとどうなるか

あとで書く

iOS 5専用のメソッドをiOS 4でも使えるようにするもうちょっといい方法

ないのなら作ってしまえpresentingViewController - Objective-Cで動的メソッド追加 - bricklifeのはてなブログ」というエントリーで、iOS 5から UIViewController に追加された presentingViewController というメソッド(プロパティ)の疑似メソッドを作成し、iOS 4でも使えるようにするという話をしました。

そこではメソッドが使われる前、例えばアプリケーション起動時などにObjective-Cランタイムの関数を使って追加する方法をとりましたが、今回は NSObject の resolveInstanceMethod: というクラスメソッドを使ってみます。

resolveInstanceMethod:

resolveInstanceMethod: というメソッドは、オブジェクトに送られたメッセージが実装されていないときに呼ばれるクラスメソッドです。Appleのドキュメントにもあるように、自分が作成したクラスで @dynamic なプロパティを作るときなどにオーバーライドして使います。なので、前回のように既存クラスである UIViewController に存在しないメソッドを動的に追加するのには適さないと思っていました。

でも、よく考えたらObjective-Cのカテゴリはその仕組み上、まったく新しいメソッドの追加だけでなくスーパークラスのメソッドをオーバーライドするのにも使えるはずで、UIViewController のカテゴリとして resolveInstanceMethod: を実装したらうまく動きそうです。

カテゴリでメソッドを上書きしてしまわないか

ただ、もしすでに UIViewController に resolveInstanceMethod: が実装されていた場合、カテゴリで同名メソッドを実装したことにより上書き問題が発生するので、結局 method_exchangeImplementations() で乗っ取りしたりしないといけません。というわけで、まずは UIViewController に実装されているクラスメソッドを以下のようなコードで調べてみました。

Class cls = [UIViewController class];
unsigned int count;
Method *methods = class_copyMethodList(object_getClass(cls), &count);
for (int i = 0; i < count; i++) {
    NSLog(@"%d:\t%@", i, NSStringFromSelector(method_getName(methods[i])));
}
free(methods);

class_copyMethodList() はあるクラスに実装されているインスタンスメソッド一覧を取得する関数です。スーパークラスのメソッドは含まれないので、今回のような調査に使えます。また、クラスとして [UIViewController class] をさらに object_getClass() したものを渡すことで、クラスメソッドの一覧を取得することができます。

で、実行してみた結果には resolveInstanceMethod: は見つかりませんでした。同じようにルートクラスである NSObject を調べると当然のことながら見つかるのですが、UIViewController のスーパークラスをさかのぼって調べてみると、結局 NSObject までどのクラスにも実装されてないようです。UIViewController の使い方を考えると、まあ当然の結果かも知れません。

とにかく、これで安心してカテゴリによるオーバーライドをすることができそうです。

実験

まずは実験。UIViewController に iOS5 というカテゴリを追加して、そこに resolveInstanceMethod: だけを実装してみます。

@implementation UIViewController (iOS5)

+ (BOOL)resolveInstanceMethod:(SEL)aSEL
{
    NSLog(@"resolving '%@'", NSStringFromSelector(aSEL));
    return [super resolveInstanceMethod:aSEL];
}

@end

これを UIViewController+iOS5.m という名前で保存してプロジェクトに追加。適当な場所で presentingViewController を使ってみます。

NSLog(@"%@", self.presentingViewController);

iOS 5でこのコードを実行したログは以下。presentingViewController はiOS 5では実装されているので、当然のことながら resolveInstanceMethod: は呼ばれていません。

2012-03-19 12:50:20.775 xxx[21025:207] <ViewController: 0x6833cc0>

次にiOS 4で実行してみます。

2012-03-19 12:50:45.968 xxx[21061:207] resolving 'presentingViewController'
2012-03-19 12:50:45.971 xxx[21061:207] -[ViewController presentingViewController]: unrecognized selector sent to instance 0x5924770
いつものエラーが続く...

おお、ちゃんと resolveInstanceMethod: が呼ばれてる! resolveInstanceMethod: の中身をまったく実装していないので結局エラーになっていますが、これをAppleのドキュメントのように presentingViewController が呼ばれたときに class_addMethod() すればちゃんと動くようになるはず…。

本番

UIViewController+iOS5.m に前回作ったように presentingViewController を偽装する関数を追加して、resolveInstanceMethod: 内で class_addMethod() します。今回は他のメソッドの定義を横取りせずに、一から作成してみました*1

#import <objc/runtime.h>

UIViewController* iOS5_presentingViewController(UIViewController* self, SEL _cmd);

UIViewController* iOS5_presentingViewController(UIViewController* self, SEL _cmd)
{
    if (self.navigationController) {
        return nil;
    }
    return self.parentViewController;
}

@implementation UIViewController (iOS5)

+ (BOOL)resolveInstanceMethod:(SEL)aSEL
{
    NSLog(@"resolving '%@'", NSStringFromSelector(aSEL));

    if (aSEL == @selector(presentingViewController)) {
        Class cls = [self class];
        IMP imp = (IMP)iOS5_presentingViewController;
        const char *types = "@@:";
        
        class_addMethod(cls, aSEL, imp, types);
        return YES;
    }

    return [super resolveInstanceMethod:aSEL];
}

@end

この状態でドキドキしながらさっきのコードを実行してみると…

2012-03-19 12:52:17.889 xxx[21189:207] resolving 'presentingViewController'
2012-03-19 12:52:17.892 xxx[21189:207] <ViewController: 0x5d33f10>

ちゃんと resolveInstanceMethod: が呼ばれた後に presentingViewController を使えるようになってます! Objective-CかわいいよObjective-C。

メリット

この方法のメリットは、なんといってもIOS 5用に書いたコードには一切手を加えず、さっき作った UIViewController+iOS5.m というファイルをプロジェクトに追加するだけでiOS 4でも動くようになるところです。iOS 4対応が必要なくなったら外せばいいだけ。なんといっても、前回のコードとは違って真の意味での動的メソッド追加になっているので気分がいいですw

デメリットは、メソッド呼び出しのオーバーヘッドが増える(と思われる)のと、将来 UIViewController で resolveInstanceMethod: が実装されたときに困ることくらいでしょうか。

まとめ

カテゴリによるメソッドオーバーライドで既存クラスに resolveInstanceMethod: を実装して、存在しないメソッドが必要になったときに動的追加する方法を紹介しました。

iOS 5で追加されたメソッドをすべて疑似メソッドとしてiOS 4上で実装できるわけはないですし、presentingViewController のように挙動を完璧にトレースできないメソッドもありますが、ファイルの追加・削除でコントロールできるのでちょっとした互換性確保には使える方法だと思います。

dismissViewControllerAnimated:completion: なんかもそれっぽい動きをするものを実装できたので、機会があったら紹介したいと思います。

*1:引数型 "@@:" は http://news.mynavi.jp/column/objc/020/index.html を参考に決定しました

変態辞書アプリ「辞書絶一門」の仕組み妄想

@TeamMOSA2 さんがリリースした「辞書絶一門」というアプリが、iOS 5内蔵の辞書からデータを抜き出してたり、さらに加工して単語にリンク貼ってたり、コピーしたテキストの意味をバックグラウンドで通知してくれたりと色々変態すごいので、仕組みを妄想してみました。

どうやって辞書データを抜き出しているか妄想

  • iOS 5で辞書を表示するには UIReferenceLibraryViewController を使う
  • リファレンス見てもわかるように中身はまったくのブラックボックス
  • でもどうせ UIWebView で loadHTMLString:baseURL: か loadRequest: してるに決まってる
  • じゃあそれをフックしたら辞書データのHTML抜けるんじゃね?

実験

こんな感じで UIWebView の loadHTMLString:baseURL: をmethod swizzlingしてみたら、string に単語の意味がHTMLで格納されてました\(^o^)/

@interface UIWebView (reference)
- (void)loadReferenceHTMLString:(NSString *)string baseURL:(NSURL *)baseURL;
@end

@implementation UIWebView (reference)
- (void)loadReferenceHTMLString:(NSString *)string baseURL:(NSURL *)baseURL
{
    NSLog(@"loadReferenceHTMLString:%@ baseURL:%@", string, baseURL);
    [self loadReferenceHTMLString:string baseURL:baseURL];
}
@end

...

Class class = [UIWebView class];
Method originalMethod = class_getInstanceMethod(class, @selector(loadHTMLString:baseURL:));
Method newMethod = class_getInstanceMethod(class, @selector(loadReferenceHTMLString:baseURL:));
method_exchangeImplementations(originalMethod, newMethod);

サンプルプロジェクト

入力された文字列を逐一辞書検索して、取得したHTMLデータを UIWebView に表示するサンプルを作ってみました。検索ボタンを押さなくても意味がインクリメンタルサーチみたいに表示されるので結構便利!

DicSample

プロジェクト一式はgithubにアップしてあります。自由に改変して使って下さい。

まとめ

この方法で申請が通るかわかりませんが、非公開な情報を使わないとなるとこんな方法しか思いつきませんでした。実際のところどうやってるのでしょう。

「辞書」や「辞書絶一門」をきっかけにいろんな工夫を凝らした辞書アプリが開発され、そのうちちゃんとしたAPIで辞書データを使えるようになる日がくるといいですね!

追記

なんと作者である @TeamMOSA2 さんから、直々にコメントを頂きました!


見事にはずれた\(^o^)/

というわけで、別の方法も妄想してみます!

ないのなら作ってしまえpresentingViewController - Objective-Cで動的メソッド追加

Objective-Cランタイムシステムを直接操作して、iOS 5から新しく追加されたメソッドをiOS 4で動かしたときにも使えるようにしてしまえ、という話です。

例題:parentViewController問題

iOSでUIViewController「親」からUIViewController「子」をモーダル表示したとき、これまでは子のparentViewControllerに親が格納されていましたが、iOS 5からはそれがnilになって、代わりに新しくできたpresentingViewControllerに格納されるようになりました。iOS 5からUIViewControllerにコンテナ機能が追加されたので、その辺色々整理されたのでしょうか。

参考:http://cocoa.hatenablog.com/entry/2011/11/24/222140

なので子から親を呼びたい場合、iOS 5以前でも動かしたいなら iOS 5で挙動が変わったUI系APIまとめ - bricklifeのはてなブログ でとりあげたように条件分岐をする必要があります。例えば dismissModalViewControllerAnimated: ならこんな感じ*1

if ([self respondsToSelector:@selector(presentingViewController)]) {
    [self.presentingViewController dismissModalViewControllerAnimated:YES];
} else {
    [self.parentViewController dismissModalViewControllerAnimated:YES];
}
Cocoaの日々: iOS5 では UIViewController.parentViewController が nil

今後はpresentingViewControllerを使うことになったわけですし、作っているプログラムがiOS 5以前をサポートしなくなったらこんなコードはなくしたいので、いまのうちからpresentingViewControllerで統一できたらいいですよね!

というわけで、UIViewControllerにpresentingViewControllerメソッドが実装されていない場合、それを動的に追加してparentViewControllerと同じ値を返すようにしてみましょう。これを実現するためにはObjective-Cランタイムシステムを直接操作します。

実装例1

まずはpresentingViewControllerをparentViewControllerとまったく同じ動作をするメソッドとして追加してみます。

#import <objc/runtime.h>

...

// 以下をアプリケーション起動時に実行する
if (![UIViewController instancesRespondToSelector:@selector(presentingViewController)]) {
    Class cls = [UIViewController class];
    Method m = class_getInstanceMethod(cls, @selector(parentViewController));
    SEL name = @selector(presentingViewController);
    IMP imp = method_getImplementation(m);
    const char *types = method_getTypeEncoding(m);
    class_addMethod(cls, name, imp, types);
}

たったこれだけで、UIViewControllerにparentViewControllerと同等のメソッドpresentingViewControllerが追加できてしまいます。ここではclass_getInstanceMethod()でparentViewControllerのメソッド内容を取得して、class_addMethod()でメソッド名(セレクタ)だけ変更して追加しています。

最初にinstancesRespondToSelector:で実装されているかどうかチェックしているので、iOS 5のときはclass_addMethod()されません。また、一度class_addMethod()したあとにまたここを通っても、すでにpresentingViewControllerは実装されているので、再度class_addMethod()されることはありません。

実装例2

今度は実装内容を変更してみましょう。iOS 5ではUINavigationControllerにpushされた場合はpresentingViewControllerがnilになるので、それをまねてみます。

#import <objc/runtime.h>

UIViewController* presentingViewControllerFake(id self, SEL _cmd)
{
    UIViewController* vc = self;
    if (vc.navigationController)
        return nil;
    return vc.parentViewController;
}

...

// 以下をアプリケーション起動時に実行する
if (![UIViewController instancesRespondToSelector:@selector(presentingViewController)]) {
    Class cls = [UIViewController class];
    Method m = class_getInstanceMethod(cls, @selector(parentViewController));
    SEL name = @selector(presentingViewController);
    IMP imp = (IMP)presentingViewControllerFake; // ← New!
    const char *types = method_getTypeEncoding(m);
    class_addMethod(cls, name, imp, types);
}

こんな感じで、idとSELを持つ引数を関数として記述しておいて、class_addMethod()するときに実装内容として渡してあげるだけで、好きなセレクタを持つ任意のメソッドが追加できてしまいます。超簡単。今回はnavigationControllerのチェックしかしていませんが、いくらでも凝った実装をすることができます。

実装例3

追加するメソッドの書き方がC言語なのが気に入らないのなら、Objective-Cでカテゴリやサブクラスなどを使ってメソッドを定義しておいて、実装を横取りすればOKです。

#import <objc/runtime.h>

@implementation UIViewController (fake)
- (UIViewController*)presentingViewControllerFake
{
    UIViewController* vc = self;
    if (vc.navigationController)
        return nil;
    return vc.parentViewController;
}
@end

...

// 以下をアプリケーション起動時に実行する
if (![UIViewController instancesRespondToSelector:@selector(presentingViewController)]) {
    Class cls = [UIViewController class];
    Method m = class_getInstanceMethod(cls, @selector(presentingViewControllerFake));
    SEL name = @selector(presentingViewController);
    IMP imp = method_getImplementation(m);
    const char *types = method_getTypeEncoding(m);
    class_addMethod(cls, name, imp, types);
}

Objective-Cのメソッド機構の実装って、セレクタの文字列がキーの関数テーブルみたいなものなので、裏側を知っていればこんな芸当もできてしまうんですね。Objective-CかわいいよObjective-C。

カテゴリ使えばよくね?

既存クラスへメソッドを追加する方法として、まず思い浮かぶのはカテゴリを使っての実装です。ただ、既に実装されているメソッドと同じセレクタを持つメソッドをカテゴリで追加した場合は、挙動が不定になるそうです。条件分岐でカテゴリを追加したりしなかったりということもできないので、今回のような場合には使えません。

__IPHONE_OS_VERSION_MAX_ALLOWEDは?

アプリケーションのビルド時ではなく実行時に判断したいのでダメです。

まとめ

Objective-Cランタイムシステムを直接操作することで、必要に応じてメソッドを動的に追加することができました。

今回の例が実用的かどうかといったらあまり実用性はないと思いますが、手法自体は互換性維持だけでなく色々応用が利くと思います。裏で何やってるかわからないライブラリをデバッグ(たまにハッキング)するときなどに、こういう話*2を知っていると捗りますよ!

*1:dismissModalViewControllerAnimated: は子から[self dismissModalViewControllerAnimated:YES]でも動いちゃうのは気にしない

*2:method swizzlingとかも

iOS5のUIImagePickerControllerQualityTypeと解像度のまとめ

iPhone 4Sを手に入れたので、UIImagePickerController で動画を撮るときの解像度やフレームレートなどをまとめ直してみた。

iOS 5 で使える UIImagePickerControllerQualityType (enum順)

  • UIImagePickerControllerQualityTypeHigh
  • UIImagePickerControllerQualityTypeMedium
  • UIImagePickerControllerQualityTypeLow
  • UIImagePickerControllerQualityType640x480 (iOS 4から)
  • UIImagePickerControllerQualityTypeIFrame1280x720 (iOS 5から)
  • UIImagePickerControllerQualityTypeIFrame960x540 (iOS 5から)


iOS 5から追加された最後の2つは、名前の通り映像をすべてIフレームで記録するという特殊なモード。そのため、その他のモードよりファイルサイズが増えるが、再生してみるとRAW映像っぽいニラニラした感じの動画になる。あとで編集する場合に適している。

解像度 (リアカメラのみ)

iPhone 3GS iPhone 4 iPhone 4S iPod touch 4th
High 640x480 1280x720 1920x1080 1280x720
Medium 480x360 480x360 480x360 480x360
Low 192x144 192x144 192x144 192x144
640x480 640x480 640x480 640x480 640x480
IFrame1280x720 480x360※ 1280x720 1280x720 1280x720
IFrame960x540 480x360※ 960x540 960x540 960x540


従来通り、High のときだけその機種の最大解像度となり、あとはどの機種でも同じサイズになる。ただし、iPhone 3GSではiOS 5から追加されたIフレームモードは非対応。すべてIフレームになることはなく、見た感じ Medium と同等の動画になる模様。

フレームレート

  • 最大29.97fps (Low のみ最大15fps)


最大と書いたのは、記録するときの状況によってこれより低くなったりするため。おそらく、なるべくメモリが空いているほうがフレームレートが高くなるのだと思う。

音声

  • 64kbps 44.1KHz モノラル(Low のみ24kbps 22.05KHz モノラル)

オマケ:iOS 5 で使える AVCaptureSessionPreset

  • AVCaptureSessionPresetPhoto
  • AVCaptureSessionPresetHigh
  • AVCaptureSessionPresetMedium
  • AVCaptureSessionPresetLow
  • AVCaptureSessionPreset352x288 (iOS 5から)
  • AVCaptureSessionPreset640x480
  • AVCaptureSessionPreset1280x720
  • AVCaptureSessionPreset1920x1080 (iOS 5から)
  • AVCaptureSessionPresetiFrame960x540 (iOS 5から)
  • AVCaptureSessionPresetiFrame1280x720 (iOS 5から)


iPhone 4Sから可能になった 1920x1080 の絶対値指定、iOS 5から登場した2つのIフレームモードに加え、352x288 という11:9のCIF(Common Intermediate Format)用解像度が登場したのがポイント。また、ドキュメントには iFrame960x540 が約30Mbps、iFrame1280x720 が約40Mbpsと書かれており、UIImagePickerController のIフレームモードで撮影したときもそれぞれ同じビットレートで記録されるので覚えておくとよい。

iOS 5で挙動が変わったUI系APIまとめ

随時追記。自分で確認していない情報もあります。

addSubveiw: しただけで viewWillAppear: が呼ばれる

viewDidAppear: なども同様。iOS 5以前はメイン以外のコントローラのは呼ばれなかったので、手動で呼ぶようにしていた場合は要注意
http://hmdt.jp/blog/?p=261

※ willRotateToInterfaceOrientation:duration: や didRotateFromInterfaceOrientation: は従来どおりメインのコントローラのしか呼ばれない

parentViewController が nil になる

親のViewControllerを取得するにはiOS 5からできた presentingViewController というプロパティを使う
http://www.comgate.jp/taiatari/archives/529

関連:iOS5でモーダルビューを閉じる

http://iphone-app-developer.seesaa.net/article/230242633.html
→ これは上記の presentingViewController を使って [self.presentingViewController dismissModalViewControllerAnimated:YES]でもOK
http://cocoadays.blogspot.com/2011/10/ios5-uiviewcontrollerparentviewcontroll.html

UIKeyboardWillShowNotification がキーボード変更時にも呼ばれる

iOS 5以前ではキーボードが現れたときだけだった。対応するには通知に登録してあるメソッド内で [[notificatioin.userInfo valueForKey:UIKeyboardBoundsUserInfoKey] getValue:&rect] を使ってキーボードのサイズを取得する。またはiOS 5からできた UIKeyboardWillChangeFrameNotification や UITextInputCurrentInputModeDidChangeNotification を使う
http://d.hatena.ne.jp/k2_k_hei/20111023/1319378195
http://iphone-app-developer.seesaa.net/article/230522433.html

関連:新しく追加された通知があるかどうか調べるには?

http://cocoadays.blogspot.com/2011/11/ios-ios5.html

shouldChangeCharactersInRange や shouldChangeTextInRange で色々

色々あるので以下参照
http://araking56.blog134.fc2.com/blog-entry-176.html

UISegmentedControlのselectedSegmentIndexを変更してもアクションが呼ばれない

iOS 5以前ではselectedSegmentIndexに数値を代入するとUIControlEventValueChangedに関連付けられたアクションが呼ばれたが、iOS 5では呼ばれなくなった。これは今のほうが仕様的に正しいと思うけど、ハマりがちなポイント