2011年4月23日土曜日

KeychainItemWrapper を改造して、複数の Keychain Item に同時にアクセス出来るようにしてみた

※2011/04/23現在での情報です。以下の問題は全て Apple に対して報告いたしましたので、ひょっとしたら将来的に修正されるかもしれません。


現在の仕事にて課金情報を安全にアプリケーション内に保存したいということになり、それならばということで iOS に最初から用意されている Security.framework を使って Keychain 領域に情報を保存するアプリを作ることにしました。この Security.framework は全てC言語にて実装されており、そのまま使うと非常に取っつきづらいです。そこで、 Apple がサンプルコードとして提供している GenericKeychain プロジェクトKeychainItemWrapper クラスを使って Keychain 領域にアクセスすることにしました。

最初は問題ないように見えたのですが、実はこの KeychainItemWrapper クラス、Version 1.2には深刻なバグがあります。それは、複数の KeychainItemWrapper クラスのインスタンスを使って複数の識別子を持った情報を保存しようとすると、同時に一種類しか保存できなくなる(二つめを保存しようとするとステータスコード25299が返却されて保存できなくなる)というものです。

原因を調査した結果、以下のページに記載されている内容が問題だとわかりました。
http://useyourloaf.com/blog/2010/3/29/simple-iphone-keychain-access.html
The combination of the final two attributes kSecAttrAccount and kSecAttrService should be set to something unique for this keychain. In this example I set the service name to a static string and reuse the identifier as the account name.
サンプルで提供されている KeychainItemWrapper クラスにはこれらの一意となるキーが指定されておらず、単に検索用の kSecAttrGeneric だけが指定されていたため、一意キーの指定がおかしくなって二つ目の Keychain アイテムが保存できなくなっていた、というわけです。

そこでこちらの問題を修正した KeychainItemWrapper クラスをご用意しました。
https://gist.github.com/938375

ライセンスは元となるAppleのオープンソースライセンスに準拠します。改変・再配布・利用すべて自由ですが、その際ソースコードの最上部にあるAppleのライセンス条項コードは絶対に改変しないでください。Apple先生に何か言われても私は知りませんよ><


■修正版 KeychainItemWrapper の使い方

基本的には通常の KeychainItemWrapper と何ら変わりません。
// インスタンスを作る
KeychainItemWrapper *wrapper = [[KeychainItemWrapper alloc] initWithIdentifier:@"com.akisute.keychain.KeychainTestApp.item1"
serviceName:@"com.akisute.KeychainTestApp"
accessGroup:nil];

// 値を保存する
// いったんresetKeychainItemしているのはmyValueがnilのとき保存されているデータを削除したいから
// 基本的に値(kSecValueData)として保存できるのはUTF-8のNSStringのみです。内部的にNSDataに変換されてKeychainに保存されます。
// それ以外の情報を保存したい場合はすみませんが未対応です>< Base64文字列にするとかJSON文字列にするとか工夫して回避してください><
NSString *myValue = @"abesi";
[wrapper resetKeychainItem];
[wrapper setObject:myValue forKey:(id)kSecValueData];

// 値を取り出す
NSString *resultValue = [wrapper objectForKey:(id)kSecValueData];
NSLog(@"resultvalue = %@", resultValue);

iOS 3の UIScrollView はスクロールが発生しない状態では内部の UIView のタッチが取れない

iOS 3以前と iOS 4以降で大きく内部実装が変わったクラスがいくつかあります。 UIScrollView はその中の一つですが、ここではその中でもかなり厄介なバグを紹介します。

iOS 3の UIScrollView は、スクロールが発生しない状態(すなわち UIScrollView.bounds.size >= UIScrollView.contentSize のとき)では内部の UIView のタッチが取れません。タッチが取れないというのは、要するに内部の UIView に実装してある touchesBegan, touchesMoved, touchesEnded などが呼び出されないということです。情報元はこちら。
http://www.iphonedevsdk.com/forum/iphone-sdk-development/1345-uiscrollview-subview-touch-problem.html
Looks like as if the scrollview blocks any touches when the content rectangle is smaller or of same size as its bounds. Really strange behaviour.
ほんと Really strange behavior ですよ Apple センセ。

回避策として、iOS 3では UIScrollView.contentSize を調整して、常にスクロールが発生するような状態にしておけば大丈夫です。 iOS 4以降でしたらこの問題は発生しません。


2011/05/02追記: @k_katsumi さんから突っ込みいただきました。
http://twitter.com/#!/k_katsumi/status/61690186664378368

以下のように delaysContentTouches プロパティと canCancelContentTouches プロパティを NO に設定する方がどうもお行儀がよいようです。
scrollView.delaysContentTouches = NO;
scrollView.canCancelContentTouches = NO;

UIButton でアニメーションする画像を表示させたい

UIButton でアニメーションする画像を表示させたいときは、 UIButton.imageView.animationImages プロパティを使うことで簡単にアニメーションを実装させることができます。さっそくサンプルを書いてみます。

以下、 iOS 3.2 および iOS 4.0 以降にて確認しております。
UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];

NSArray *animationImageNames = [NSArray arrayWithObjects:@"animation1.png", @"animation2.png", @"animation3.png", nil];
NSMutableArray *animationImages = [NSMutableArray arrayWithCapacity:[animationImageNames count]];

for (NSString *animationImageName in animationImageNames) {
UIImage *image = [UIImage imageNamed:animationImageName];
[animationImages addObject:image];
}

[button setImage:[animationImages objectAtIndex:0] forState:UIControlStateNormal]; // 大事
button.imageView.animationImages = animationImages;
button.imageView.animationDuration = 2.0;
[button.imageView startAnimating]; // 大事、忘れるとアニメーションしません

[self.view addSubview:button];
UIButton.imageView.animationImages プロパティを使う際に注意する点がいくつかあります。
  • 必ず最初に setImage:forState: メソッドを使って何らかの画像を表示させるようにすること。通常の画像を表示させるようにしないと、 UIButton.imageView が画面に表示されません。
  • iOS 4.0以降ではこのプロパティでセットした画像は UIButton の frame に合わせてリサイズされて表示されますが、 iOS 3.2 以前ではリサイズされずそのままの大きさで表示されますので、注意が必要です。

2011年4月17日日曜日

Xcode 4 で scheme が My Mac 64bit になって iPhone 向けのビルドが出来なくなった時の対処法



最近会社のプロジェクトで使う Xcode を Xcode 4 に乗り換えたのですが、全く新しく作り直された バグだらけでまともにビルドすら出来ない上に無料じゃなくなったどうしようもない出来映えの IDE ということで、何かとトラブルが多いようです。今回はその中でももっとも頻発したものをご紹介します。

たまに iOS 向けのプロジェクトを Xcode 4 で開いたときに、左上の scheme 選択欄に My Mac 64bit と表示されてしまって iPhone シミュレータや実機でビルドが出来なくなることがあるようです。

対処法はこちら。
http://stackoverflow.com/questions/5319251/xcode-4-the-selected-run-destination-is-not-valid-for-this-action

Build Settings を開き、 Base SDK の設定をいったん Mac OS X SDK に変更してから、もとの iOS SDK に戻すと問題が解決されるようです。これで一安心ですね。

Xcode 4.0 と Xcode 4.0.1 で再現することを確認しました。 Xcode 4.0.2 では未確認です。

Xcode 4 で build target 名にスペースが含まれているとビルド時に 100% エラーになる



タイトルからしてひどい出落ちですが、 Xcode 4 で build target 名にスペースが含まれているとビルドを行った際に 100% ビルドに失敗します。

原因がまたお粗末で、 Xcode 4 は build target 名をビルド成果物の中間配置ディレクトリの名前に使っているのですが、スペースをエスケープしていないためコンパイラが正しくパスを解釈できずに落ちているようです。がっくし。



Xcode 4.0 での調査結果です。 4.0.1 または 4.0.2 では修正されている可能性がありますが、いずれにせよスペースはさけたほうが良さそうです。

UINavigationController に管理されている UIViewController の view のサイズを変更したい

たとえばアプリ内の固定の位置に広告を突っ込みたいときなど、 UINavigationController に管理されている UIViewController の view 構造を操作したい場合があると思いますので、調べてみました。


■UINavigationController.viewの中身

iOS 4 ですと以下のようになっています。



最上位の UINavigationController.view に相当するのが UILayoutContainerView で、その下に UINavigationTransitionView があり、そのまた下に UIViewControllerWrapperView というのがあって、こいつが表示される UIViewController.view をラップして表示しているという仕組みになっているようです。

おそらく iOS 3.2 でも同じだと思います。 iOS 3.1.3 以前についてはわかりません。


■実際にやってみる

いろいろ試してみた結果、もっともうまくいきそうなのは以下のように、 viewDidAppear のタイミングで UIViewController.view.superview に対して操作を行う方法であることがわかりました。
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
self.headerView.frame = CGRectMake(0, 0, 320, 100);
self.view.frame = CGRectMake(0, 100, 320, self.view.superview.frame.size.height - 100);
[self.view.superview addSubview:self.headerView];
}

■問題発生

ところがこの方法では画面遷移時のアニメーションが発生すると上記の例で追加した headerView の表示が無茶苦茶に壊れてしまいます。またタイミングを誤ると headerView のサイズが self.view.superview.bounds の大きさに補正されてしまいます。恐らくは UINavigationTransitionView とかいう private な view がなにやらやっているのでしょう。アニメーションが発生しない範囲であれば問題なさそうですが、いまいちです。


■結論

横着しないで表示される UIViewController の view で何とかするしかないみたいですね><