2012年11月22日木曜日

UIViewAnimationに渡したブロックはどうなるの?

UIViewAnimationを入れ子構造にしてselfをリークさせる、高度なヒドいコードを修正していたら疑問になったんだけど、UIViewAnimationに渡したanimationブロックはその後どうなるんだろうか。

view.layerに対してremoveAllAnimationsを実行すれば、UIViewアニメーションを中断できることから、animationブロック内部の処理は、CAAnimationに変換されて、viewのlayerに渡されるということは推測できる。

iOS View プログラミングガイドでは、UIViewのアニメーションメソッドが対象とするプロパティはview自身のものに限定されているような書き方をしているのに、実際にはview.layerに対する処理を書いても問題なく実行されることを不思議に思っていたんだけれど、layerにaddAnimationしているならば当然。

するとcompletionブロックはCABasinAnimationのdelegateインスタンスに保持されるんだろう。非形式プロトコルのdelegateメソッド、animationDidStop:finished:から実際のブロック実装が呼び出されるかたちかな。実際はメソッド実装の書き換えとかもっと高度なことをやってそうだけど。

このCore Animationのdelegateメソッドは、Cocoaのメモリ管理のレアな例外で、保持の向きが逆になっている。(Animationがdelegateを保持する)

まとめると、

  • アニメーションブロックはCAAnimationに変換されて、view.layerにaddAnimationされる。
  • 完了ブロックは、CAAnimationのdelegateメソッドを実装したインスタンスに保持されて、さらにdelegateがCAAnimationに保持される。
  • もし完了ブロックにviewを保持するインスタンスをキャプチャした場合、循環参照が発生する。

例えばUIViewControllerにアニメーション処理を記述したならば、UIViewController → view → layer → animation → animationDelegate → completationBlock → UIViewController…という循環が生じるわけだ。

そうなっても、完了ブロックが終了してUIViewControllerが解放されれば、循環は解消するので普通は心配いらない。

---

循環を起こすコードは例えばこんなの。

-(void)animation
{
    [UIView animateWithDuration:1.0f delay:0.0f options:UIViewAnimationCurveEaseInOut animations:^{
        self.theView.transform = CGAffineTransformMakeTranslation(0200);
    } completion:^(BOOL finished) {
        NSLog(@"%@ %@",selfself.theView);
        [UIView animateWithDuration:1.0f delay:0.0f options:UIViewAnimationCurveEaseInOut animations:^{
            self.theView.transform = CGAffineTransformIdentity;
        } completion:^(BOOL finished) {
            NSLog(@"%@ %@",selfself.theView);
            [self animation];
        }];
    }];
}
入れ子にしなくても循環するけど見た目がアニメーションにならないので。

これで別画面に行くとどうなるかというと、二つのcompletationブロックでNSLogが高速に吐き出され続ける。layerの実体を管理してるのはviewではなく、描画システム側だから描画処理自体はスキップされて、延々と呼び出しだけ繰り返される…という感じかな。

ちょっと考えれば再帰呼び出ししてるからアウトだと理解できそうなもんなんだけど、厄介なことに単なる再帰呼び出しと違ってスタックオーバーフローが起きないから発覚を遅らせる。完了ブロックが完了ブロックを呼び出す度に元のブロックは律儀に解放されてるのかな。

こういう変に手の込んだバグコードを書きそうになったら「もっとシンプルな方法があるんじゃない?」と自問すべき。大抵はもっとシンプルで、そしてその方が正しい実装法の書き方が存在する。

上のコードの場合、こう書き直せる。

-(void)animation
{
    [UIView animateWithDuration:1.0f
                          delay:0.0f
                        options:UIViewAnimationOptionCurveEaseInOut UIViewAnimationOptionAutoreverse UIViewAnimationOptionRepeat
                     animations:^{
        self.theView.transform = CGAffineTransformMakeTranslation(0200);
                        } completion:^(BOOL finished){
                            NSLog(@"finished");
    }];
}
UIViewAnimationOptionRepeatを指定すれば、そのアニメーションは繰り返され続けるから、それこそ無限ループを発生させているようで不安になるのかもしれないけど、画面遷移などでlayerが無効になると自動的にアニメーションが停止し、completionに記述された処理が実行される。

UIViewAnimationを入れ子にする処理はAppleのサンプルもやってるけど、Blocksの理解が甘いと変な罠に引っかかるから、真似しない方が良いと思う。

入れ子にしないと実現できないような、複雑なアニメーションをやりたければ、Core Animationを勉強してkey-frame animation辺りで実装した方がシンプルにできる。Core AnimationはCore系の中では最弱なので簡単に覚えられるはず。

----

最近修正してるヒドいコードの数々は、自分より立場の偉い人間が書いてるので、文句が言えない。せめて同じ轍を踏む人間が減りますように。

2012年11月20日火曜日

NSDateFormatterをalloc initのまま使うな!

こんなの入門書に載っているレベルの話なんだけど。

NSDateFormatterは、NSDateを "yyyy年MM月dd日" みたいな表現にするのに使うフォーマッタなんだけど、これをalloc initで生成してそのまま使うと、iOS本体のユーザー設定が反映されるようになっている。

例えば西暦ではなく和暦を設定している場合、yyyy年は2012年ではなく24年になる。ユーザーの本体設定を反映することで、ユーザーにとって違和感なく表示するための仕組みだが、プログラム側では現在の西暦を取得したいときもある。そういうときにalloc initのまま使ってはいけない。

フォーマッタ使うときは、ここまでテンプレとして書くとよさげ。

NSDateFormatter *formatter = [[[NSDateFormatter alloc] init] autorelease];
formatter.calendar = [[[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar] autorelease];
formatter.locale = [[[NSLocale alloc] initWithLocaleIdentifier:@"ja_JP"] autorelease];
formatter.timeZone = [NSTimeZone timeZoneWithAbbreviation:@"JST"];

海外に出すことを考えないアプリ限定になるけれど、カレンダーをグレゴリオ暦(この呼び名に馴染みは薄いけどただの西暦)にして、ロケールを設定(iOSは12時間表記時にユーザーロケールのままだとHHを正しく解釈しない)して、タイムゾーンも設定しておけば、フォーマット書式は確実に指定した通りになる。

iOS6から「YYYYとyyyyを間違える」ときに返ってくる値が変わったみたいでこれもバグの原因になっていたりするみたいだけど、書式文字の意味が分かってたらこんな間違いをしないってゆーか、ちゃんとリファレンスは読みましょうよ…。

---

以下は余談だけど、時間を比較したり、データベースに入れたり、ネットワークに送信するのに、NSDateFormatterで書式化した文字列使うな!と言いたい。

Objective-C内で日時を扱うならNSDateの同値・比較メソッドを使うべき。外部とやりとりするのであればUNIX時間で管理するのが一番良い。

例えば次のメリットがある。
  • データ量が少ない
  • 比較コストも安い
  • NSDateとUNIX時間を相互変換するメソッドが用意されているため復元も容易
  • SQLiteには厳密な時間を表現する型がないが、FMDBを使えば、NSDate←→UNIX時間の相互変換を自動でやってくれる(後述)

そもそも本当にNSDateFormatterを使わなければならないケースなのか考えるべき。あくまで外部に表示するとき、
  • ユーザーの望むフォーマットやロケールで日付を表示したい
  • ユーザーが望むフォーマットで入力した日付文字列を変換したい
  • 明示的に指定したフォーマットで日付文字列を表示しなければならない
こういうときにだけ使うものだと考えた方がいいと思う。

内部で日付計算するとき、外部に日付を書き出すときに使用すべきではない。

どうしても文字列で保存しなければならない、そうしなければ誘拐された娘の命が危ない、とかそういう場合でも、せめてISO8601とか標準書式に則った書き方にするのが無難。

場所によって記法が混在するとか一番ダメなパターン。

----

ちなみにNSDateはクラスクラスタなので、おそらくロケールやカレンダーなどの違いに応じて、それぞれの表現に適した内部データ構造を持っている。

異なる表現の日付型を橋渡しするプリミティブメソッドとしてtimeIntervalSinceReferenceDateが定義されているんだけど、これはUNIX時間ではなくて、グリニッジ標準時で2001年1月1日を起点にしている。NSDateの独自クラスを実装するわけでもなければ気にする必要はないけど、timeIntervalSince1970(UNIX時間)と微妙に紛らわしいので注意。

あと、timeIntervalSince...系のメソッドはdouble型の値を返すけど、それが期間の表現であるとソース上で明示するためにTimeIntervalを使用すること。

----

SQLiteは日付型が用意されていない。仕様上、日付の内部表現として、INTEGER、REAL、TEXTの3方式を選択できることになっている。

iOSアプリで使う場合、TEXTは論外。文字列をパースするコストや容量が無駄なだけ。

残るはREALとINTEGERの2択だけど、「TimeIntervalがdoubleなのだから、REALの方が相応しいのでは?」と思うところだけど、SQLite3の仕様上、REALはユリウス日数(ユリウス暦の紀元前4713年1月1日を起点にした日数)で使用するもので、Unix時間(グレゴリオ暦1970年1月1日を起点にした秒数)を格納するのはINTEGERになる。

こうして格納した日付は、FMDBを使うことでNSDateとして取り出すことができるし、SQLite3の日付関数を使って特定の日時のデータを抽出するクエリを書くこともできる。

2012年11月19日月曜日

条件演算子(三項演算子)の言語の違い

Objective-CでDictionaryの内容をクエリ文字列化するメソッドを、

NSMutableString *result = [NSMutableString string];
__block BOOL firstQuery = YES;
[param enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
 (firstQuery) ? firstQuery = NO : [result appendString:@"&"];
 [result appendFormat:@"%@=%@",key,obj];
}];

こんな風に書いた。(勿論実コードはパーセントエンコードとかちゃんとやってる)

Objective-Cは相変わらず好きになれないけど、for文なしでコレクションの走査ができるブロックの簡便さは良いね。

それで、Javaでも似たことをやろうとしたらコンパイルが通らなかった。

C言語では三項演算子 (条件式) ? 式A : 式B の、式Aと式Bはどんな内容でも評価される。もちろん左辺式が存在する場合に、式Aと式Bのいずれかが代入先の型と違うならば、コンパイルエラーになる。逆に言えば左辺式がなければ、上記みたいに返り値型がvoidのメソッド実行もできる。

対して、Javaでは式Aと式Bの型には互換性がなければならない。式Aと式Bに共通のスーパークラスが存在したり、同一のインターフェースを実装しているなど、オートボクシングによって両者が同一の型とみなせる状況でのみ使用することができる。

C#とかはもうちょっと厳しくて、キャストなしで式Aが式Bに代入できる(またはその逆)場合にしか、三項演算子を使うことができないらしい。

三項演算子がネストした際の挙動が言語ごとにかなり違う…というのは知っていたけど、こんな基本的なところに違いがあるもんなんだねえ。

参考:Java5で条件演算子(三項演算子)に仕様変更があった! - 地平線に行く

2012年11月7日水曜日

User Defined Runtime Attributes

InterfaceBuilderで各要素に対して存在するこの設定項目。

「ユーザー定義の実行時属性」とかいうと分かりにくいけど、やってることは単に実行時にキー値コーディングを用いてプロパティに値を設定するだけ。

これを使用することで見た目に関わるプロパティの設定を、viewDidLoadからxibに移す事ができる。「ビューとロジックを分離させたい」という用途にうってつけのように思える。

しかしインテリセンスみたいな補完は一切仕事しないし、スペルミスってたりプロパティ自体存在してなかったりしたら、実行時にUndefined Keyで落ちる。

 しかも設定できる要素にかなり制限があって、

  • 整数は指定できるけど、小数は指定できない。
  • UIColorは指定できるけど、CGColorは指定できない。

これらの制約によってlayer.cornerRadiusを操作するという用途には向かないし、layer.borderColorは設定できない。角丸にしたり影付けたりみたいなのは、viewDidLoadでやるしかない。ギリギリ使えるのは、UITableViewの背景を透過させるのにbackgroundViewにnilを代入したりとか…。

謎なのが「Localized String」の設定項目の存在で、これを使えばコード上でLocalizedStringsを用いて文字列を書き換えているコードを一掃できるように見える。しかしiOSでもOSXでも機能しないし、調べても「使えない」「ドキュメントもない」という報告ばかり。仮に使えたとしてもgenstringで検出できないので、利用価値はゼロだろう。

「iOS5 プログラミングブック」ではこの属性の存在について触れているんだけど、実際に使った上で書いてるんだろうか?

また「User Defined Runtime Attributes」で設定したプロパティは、実行時にキー値コーディングで設定されるだけなので、当然Interface Builder上に反映されるわけもなく、設定できる内容も中途半端なことからIB上とコード上に処理が分散して、かえって分かりにくくなる可能性の方が高いので、多人数プロジェクトには向かない。