カテゴリー : 開発

viewWillLayoutSubviewsが意図せず再帰呼び出し


UIViewControllerにviewWillLayoutSubviewsってメソッドがある。ビューコントローラが管理しているビューが再レイアウトされるときに呼び出されるもので、iOS 5から追加された。

このメソッドって、UIViewControllerが直接管理しているviewのframeが変更されたときだけ呼び出されると思っていたら、そのsubviewで再レイアウトが必要なときにも呼び出されちゃう事があるのね。しかも、すぐその場で。

デバッグ中にぶち当たった問題は、viewWillLayoutSubviewsの中でUIButtonの背景画像を変更したら、それがボタンの再レイアウトを誘発したらしく、その場で再びviewWillLayoutSubviewsが呼び出された。再帰的に。マジですかー。そんなもん、想像してなかったよ。

まぁ、なんべん呼び出されても大丈夫なようにはしてるんだけど、気持ち悪いよなー。

PDFとの格闘はToUnicodeへ


先日、PDFからテキスト抽出するためにCIDからUnicodeへのマップを作っている、と書いたら、「Fontディレクトリに含まれているToUnicodeを使うべし」というコメントをいただいた。

まったくもってその通りで、PDFの仕様書には、テキストを取得するにはToUnicodeを使えばいいよ、と書いてある。だけど、これは必須項目ではないので、巷にあふれているPDFファイルには、ToUnicodeが含まれているものもあれば、含まれていないものもある。というか、含まれているの見た事ねーよ!

と嘆いたら、「Appleの開発者向け日本語ドキュメントには含まれているよ」とのコメントが。早速確かめてみると、あったよ!これかー。なので、それをサンプルとしてToUnicode取得とストリームスキャンのためのコーディングをしている。

ちなみにこのPDFは、Creatorは不明。ToUnicodeが含まれていないPDFは、CreatorがAdobe Acrobatだったりする。Acrobat、貴様、ちゃんと入れとけよ。

CGPDFだと、Fontディレクトリから、ToUnicodeストリームを取得するところまでは、APIを使ってできる。でも、このストリーム(CGPDFStream型)からは、生データを取得できるのみでそれ以上のサポートはなし。あとは手作業で取っていくしかない。シクシク。ちなみに、PDFページのストリームを表すCGPDFContentStream型だと、もうちょっとサポートがあって、operatorのパースまでしてくれる。

でも、標準のAPIに機能が用意されてなくとも、その場ですぐにC言語にスイッチして低レベルのアクセスができるってのは、ほんっとに嬉しいし安心感がある。Objective-Cの最高の強みは、C言語をサブセットとして持っている事だね。スクリプト言語やマネージドコードとかいった輩は、どうにもならない檻の中だ。

PDFとの格闘で、CIDからUnicodeへのマップを作る


引き続き、PDFと格闘中。

PDFの内部データ触るには、CGPDF系の関数を使えばいい。その辺りは、iOSが提供してくれるので、ありがたく使わせてもらう。

そこまではいいんだけど、PDFから取得できるテキストデータは、CIDの形になっている。表示するだけならいいんだけど、コピーや検索するには、これをUnicodeに変換しないといけない。CIDからUnicodeへの変換が、ない。だいたいみんな、ここでひっかかる。

たぶん、Appleは持っているはず。PDFでコピーできるから。OS Xでは、PDF Kitを使えばテキスト取得できる。でも、iOSではない。

ないものは作ったれー!ということで、CIDからUnicodeへのマップを作っております。非常に原始的に、Adobe-Japanの仕様書を見て、対応するUnicodeの値を調べて、テキストエディタで記入する。

こんな方法しかないのかー!?やってみると、縦書き文字なんかもあるから、機械的にできないところもある。手でやるしかないのかー。

しかし、これさえ出来上がれば、PDFのコントロールは完全に我が手に。なるはず。

UIImageからハッシュ値を求めるときのパフォーマンス


画像を開いた2つのUIImageがあるとき、それが同じ画像かどうか判断したいときがある。いちばん負荷が少ないのは、画像を開くときのパスなりURLなりから判断する事だけど、そうもいかなくて、UIImageインスタンスを直接比較するしかない、ってときがある。

こんなときはハッシュを使うのが一般的だ。ハッシュを使えば、大きなバイナリデータも、その特徴を表す16バイトのデータを取得できる。これを比較してやればいい。iOSの場合は、CommonCryptoフレームワークにMD5ハッシュを取得するための関数があるので、これを使う。

となると次の問題は、どうやってバイナリデータを取得するか、ということだ。つまり、UIImageから、その画像を表すデータを取得したい。ざっと思いついたのは、次の3つの方法だ。

1. UIImagePNGRepresentationを使ってPNGデータを取得する

unsigned char   hash[16];
NSData*         data;
data = UIImagePNGRepresentation(image);
CC_MD5([data bytes], [data length], hash);

2. UIImageJPEGRepresentationを使ってJPEGデータを取得する

unsigned char   hash[16];
NSData*         data;
data = UIImageJPEGRepresentation(image, 1.0f);
CC_MD5([data bytes], [data length], hash);

3. CGImageからCGDataProviderを取得してビットマップデータを取得する

unsigned char       hash[16];
CGDataProviderRef   dataProvider;
NSData*             data;
dataProvider = CGImageGetDataProvider(image.CGImage);
data = (NSData*)CFBridgingRelease(CGDataProviderCopyData(dataProvider));
CC_MD5([data bytes], [data length], hash);

どの手法であっても、比較すること自体は問題ない。あとは、パフォーマンスの差だけだ。

ということで実測してみた。iPad 3rdで、1255 x 1322サイズの画像のハッシュ値を求めた。結果は次の通り。

1. PNG 1.647941sec

2. JPEG 0.442495sec

3. Data Provider 0.323576sec

ということで、Data Providerを使う手法がいちばんパフォーマンスが良かった。これを使うのがいいかな。

ちなみに、実行時間のほとんどはバイナリデータを取得する処理にかかっていた。ハッシュを求める計算が占める割合は、とても低かった。PNGへの変換は、エライこと時間がかかるわけね。

Map Kitのアノテーションは、バックグラウンドで触らない方がいい


音楽のある情景」の絡みで、Map Kitをいじることが多いんだけど。

地図ベースのアプリでは、地図上に多数のアノテーションを作る事になる。ちょっと油断すると、その数は軽く1,000を超える。そこで、このアノテーションの作成や更新をいかに軽くするか、ということが重要になってくる。

ある程度の数がある以上、どんなにがんばっても処理時間は0にはならないので、バックグラウンドに持っていく事になる。そのとき、どこまでもバックグラウンドでやって、どこからをメインスレッドでやるかの見極めがとても重要になる。

Cocoaプログラミングの原則では、ビューに関する操作はメインスレッドでやることになる。じゃあ、ビューじゃなければバックグラウンドでもいいかというと、これがケースバイケースだったりする。

今日はまっていたのは、地図上でアノテーションを表すMKAnnotation。MKAnnotationはプロトコルなんで、それを実装したクラスはこちらで作る事になる。特に親クラスは指定されない。これだったら、バックグラウンドで触ってもいいだろう、と思ってたら、どうもいかんらしい。バックグラウンドスレッドからアノテーションのcoordinateを触ると、どうにも挙動がおかしい。

想像だけど、一度アノテーションがMKMapViewに追加されるとそっちの管理下におかれるみたい。この状態でアクセスするとビューを触るのと一緒になって、バックグラウンドからのアクセスは厳禁、って理屈だろうか?

とりあえず、メインに持っていったら期待通りの挙動になった。難しいのー!

プライバシーの連絡先でアプリが死ぬ


iOS 6で、環境設定のプライバシーに連絡先の項目があって、システム標準のアドレスブックへのアクセスを許可するかどうかを、設定できる。

で、設定のオン/オフを切り替えると、そのアプリが死ぬみたい。AddressBookフレームワークのAPIにアクセスしていようが、いまいが。アプリ側には、デリゲートメソッドも通知も飛んでこねぇ。SIGKILLされた。

そういうもん?そういうものなの?プライバシーのためなら、アプリなんか死んでもいいの?

deprecatedには、こまめに対応しないといかん


UIImageを引き延ばして利用するために、昔からstretchableImageWithLeftCapWidth:topCapHeight:っていうメソッドがあった。これ、iOS 5.0でdeprecatedになって、resizableImageWithCapInsets:を使え、ってことになっている。

Deprecatedになったメソッドの嫌らしいところは、この宣告がなされたとしても、とりあえずは使える。ビルドは通るし、実行もできる。だけど、バージョンアップを繰り返してある日気付くと、あれ?なんか動作がおかしい。調べてみると、deprecatedのせいだったりする。

最近問題になっていたのは、UISliderのsetMinimumTrackImage:とsetMaximumTrackImage:。スライダーの背景画像を設定するためのメソッドだ。これが、iOS 5.0までは問題なかったのに、6.0からどうも挙動が怪しい。描画が乱れるときがある。でも、毎回必ずって訳でもなくて、ちゃんと動いているときもある。

なんでだろう、とあちこちいじってたら、設定する画像をstretchableで引き延ばしていたのが問題だった。resizableにしたら、期待通りの動作になった。分かりにくいよー。

一見動作しているように見えても、後のバージョンのことを考えると、deprecatedにはこまめに対応するのが必要だ、と思わせる事でした。

Google Maps SDK for iOSがきた


Google Maps SDK for iOSがきたー!

ということで、ダウンロードして絶賛テスト中。

SKStoreProductViewControllerでハマったこと


iOS 6から、SKStoreProductViewControllerっていうクラスが追加された。アプリ内でApp Storeの購入画面を表示できるスグレもの。ユーザの導線をアプリ内から逃がさずに、他のアプリを紹介できる。

こいつには、loadProductWithParameters:completionBlock:っていうメソッドが用意されていて、これにストアで表示させる項目をいろいろと設定する。

    // Create controller
    SKStoreProductViewController*   controller;
    controller = [[SKStoreProductViewController alloc] init];
    controller.delegate = self;

    // Set parameter
    NSDictionary*   parameters;
    parameters = [NSDictionary dictionaryWithObjectsAndKeys:
            @"500860946", SKStoreProductParameterITunesItemIdentifier,
            nil];

    // Present controller
    [self presentViewController:controller animated:YES completion:^() {
        // Load product
        [controller loadProductWithParameters:parameters
                    completionBlock:^(BOOL result, NSError* error)
        {
            // For success
            if (result) {
                //...
            }
            // For error
            else {
                //...
            }
        }];
    }];

このコードで、表示できる事が確認できた。

ちなみにドキュメントの方を確認すると、このメソッドの使い方としてこう書いてある。

In most cases, you should load the product information and then present the view controller.
(多くの場合、プロダクト情報を読み込んでから、ビューコントローラを表示するべきでしょう)

これ、そのまんまの通りに解釈すれば、loadProductWithParameters:を呼んで、それが成功したらpresentViewController:する、って思うでしょ。始めそれでやってみたんだけど、それだとアクセスに失敗したときにエラーも何も返ってこない。completion blockがまったく呼ばれないの。最初に実験したとき、間違ったプロダクトIDを与えていて、それでなんの音沙汰もなし。

なんでじゃー、といろいろいじくっていたら、presentViewController:を呼んでおかないと、エラーのときのcompletion blockは呼ばれなかった。ということで、なにはともあれpresentViewController:することにした。でもそれなら、ドキュメントに書いてある事まぎらわしくねー?

モーダルビューとは何なのか


そもそもの発端は、UIViewControllerをモーダルで表示して、それを半透明にしたかった。ほら、モーダル表示すると、下からシュワンと出てきて、重なって表示されるでしょ。このとき、上のビューを半透明にしたらかっこいいんじゃないか、って思ったんだ。

やってみたら、できなかった。なんか、真っ黒になった。Google先生に聞いてみた。modalPresentationStyleをUIModalPresentationCurrentContextにすればいいよ、っていう記事が見つかる。確かにこれでできた。でも、なんか微妙に動きが怪しい。UIModalPresentationCurrentContextってやつも、何なのか、ドキュメントを読んでも、いまひとつ要領を得ない。

てか、そもそもモーダル表示って何なのよ?addSubview:するのと何が違うの?気になったので、少し調べてみた。モーダル表示を何度も重ねる事のできるサンプルを作った。あるUIViewControllerをモーダル表示して、そいつに対してまたモーダル表示して、ということでモーダルのモーダルのモーダルを実現する。その状態で、ビュー階層を調べてみた。

すると、いくらモーダルを重ねても、ビュー階層に残っているのは、一番上に表示されているUIViewControllerのビューだけだ、ということが分かった。モーダルの下になったコントローラ、つまりpresentViewController:animated:completion:を呼び出した側は、モーダルが表示されたらビュー階層から外れるんだ。知らなかったー。

これで、冒頭の半透明表示がうまくいかない理由が分かった。半透明にはなってたんだけど、下に透けて見えて欲しかったビューが、もう外れてしまっていたんだね。なるほど、納得。

じゃ、modalPresentationStyleをUIModalPresentationCurrentContextにするとどうなるかっていうと、この場合はビューが外れなかった。モーダルを重ねたら、その分すべてのビューが追加されたままになっている。うーむ、なるほど。それはそれで、嫌な事が起きそうだなー。

半透明にしたかったら、自分で制御可能な分、addSubview:した方が安全な気がする。ただその場合、viewWillAppear:とかviewDidAppear:とか、addChildViewController:とかを自前で呼ばないといけない。その変が面倒だし気をつけないといけないところだ。