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月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上とコード上に処理が分散して、かえって分かりにくくなる可能性の方が高いので、多人数プロジェクトには向かない。