2012年9月29日土曜日

iOSのレイヤアニメーションメモ

キー値コーディングの知識は、iOSアプリではほとんど無駄だと思っていたけど、Coreフレームワーク群はプロパティの生まれるObjective-C 2.0以前の、つまりキー値コーディングの時代の知識を必要とするドキュメントが多いので、目を通しておくだけでも理解できなかったドキュメントが頭に入るようになると思うのでオススメ。

---

レイヤアニメーション(Core Animation)関連で調べていて躓いた点のまとめなど。

---

レイヤって何?

UIKitではViewが最小の粒度だったが、さらに下位のフレームワークでは最適化のためにViewは描画部分と、ユーザーインタラクションの部分を別の仕組みで扱っている。

このうち描画を担当しているのがレイヤ。

このため、UIViewアニメーションでは当たり判定も同時に移動していたが、レイヤアニメーションでは当たり判定は追従しないという違いがある。

その分レイヤアニメーションの方が性能が高く、出来ることも多い。

---

CAAnimationとかCABasicAnimationとかいろいろあってワケ分からない。

CAのプレフィクスはCoreAnimationフレームワークに属することを意味する。

全てのアニメーションクラスの抽象クラスがCAAnimation、その他はサブクラス。クラスの関係はリファレンスに載っている。

transformやalphaなどのプロパティ値の変化を数学的に補完するCAPropertyAnimationがあり、そのサブクラスとして開始状態と終了状態を指定するCABasicAnimation、配列で値を渡すことで複雑な動きを表現するCAKeyframeAnimationとがある。

それとは別に異なるレイヤへの切り替え処理を行う、トランザクションアニメーションを扱うCATransitionがある。

---

イージングを使いたい

iOSのアニメーションは確か何もしなくてもEaseInEaseOutのイージングが機能していると思うけど、組み込みで使えるイージングはLinear、EaseIn、EaseOut、EaseInEaseOutの4種類しかない。

カスタムなイージング関数を使う場合、レイヤアニメーションのtimingFunctionプロパティを自作のCAMediaTimingFunctionクラスのオブジェクトに置き換える。

「ベジェ曲線の制御点を2点指定する」方法で簡単に作れるが、それ以上の制御点が必要な複雑なアニメーション(例えば振動して収縮する系統など)はサブクラスを作ってgetControlPointAtIndex:values:をオーバーライドすればよさげ。

嘘でした。複雑なアニメーションはKeyFrameAnimationとしてしか作れないみたいです。

---

アニメーション終了時に処理を書きたい

普通にやるならアニメーションオブジェクトのdelegateプロパティを指定して、デリゲートメソッドを実装する。

その辺の処理をラップして、ブロック構文で済ませられるようにすると良さげ。

---

アニメーション終了後処理でアニメーションの識別をしたい

普通にデリゲートメソッドとして書く場合、デリゲートオブジェクトは大抵呼び出し元一つになるので、「どのアニメーションの終了処理なのか?」を知る必要がある。

そういうデリゲートの残念さを回避するのにブロック構文を使うといいんだけど、使わない場合は落とし穴が二つあるので注意。

まず、アニメーションは終了時点で識別するには、引数で渡したアニメーションと、レイヤに与えたアニメーションが同じオブジェクトかを比較するしかない。

しかし、デフォルトではアニメーションが終了した時点で解放されるので、removedOnCompletionプロパティにNOを渡して、解放されないようにしなければならない。

ただしこのままだと与えたアニメーションが保持し続けられてしまうため、同じレイヤに複数のアニメーションを実行させたときに、終了後にレイヤが不可視になるなどの振る舞いが発生してしまう。なので、終了処理でremoveAllAnimationsメソッドなどでアニメーションを手動解放する必要がある。

---

レイヤアニメーションを使うメリットって?

UIViewアニメーションで処理がもたつく場合は、レイヤアニメーションに切り替えることで改善される可能性はある。

カスタマイズしたイージング関数が使える。これはデフォルトで選べる選択肢が少なすぎるのが悪い気がするけど。

また3Dトランスフォームが可能。MacOSXやiPhoneにカバーフロー表示があるのは、偏にCore Animationが3Dトランスフォームアニメーションをサポートしているから。

---

実はUIViewAnimationでもレイヤを操作できる

CAAnimationの低レベルAPIを使用しなくても、レイヤを操作したアニメーション自体はできる。

UIViewのbeginAnimationでも、iOS4以降のanimateWithDuration:animations:でもOK。layerの値を操作するとちゃんと補完され、レイヤアニメーションが実行され、終了後にはViewの判定も移動している。CATransform3Dを使用してもまったく問題なし。

不可能だと書いてある本やサイトも存在するので、iOS5前後で可能になったのでしょうかこれ…。

2012年9月11日火曜日

iOSにおけるFirstResponderについて

ファーストレスポンダとは、Cocoaにおいてフォーカスが当たっているGUI要素のことです。そしてタッチ以外のイベントを、最初に受け取ることになるオブジェクトです。

そんなことは入門書にも腐るほど書いてあるので分かっていると思います。そのはずなのに「InterfaceBuilderのFirstResponderプレースホルダの使い方は分からない」「使ってみたけど期待した動作をしなかった」という人向けの記事です。

---

タッチイベントが発生すると、UIApplicationからUIWindowへ、さらにUIViewControllerからUIViewへと下層に向かってヒットテストが発生します。そして最下層(ユーザーから見ると最前面)のビューがヒットテストビューとなり、受け取ったイベントを処理する責務を負うのです。

もしヒットテストビューがイベントを処理できなかった場合は、替わって上層のUIViewやUIViewControllerが対処に当たります。そして遡り続けても誰も処理できなかった場合、そのイベントは無効とみなされ破棄されます。

---

では「文字入力」や「シェイクモーション」などのタッチ以外のイベントはどのオブジェクトが受け取り、またどうその責務を伝播させればいいのでしょう?

その答えがファーストレスポンダレスポンダチェーンなのです。

特定のオブジェクトにbecomeFirstResponderメッセージを送ると、ファーストレスポンダオブジェクトになります。そしてタッチ以外の発生したイベントは全て、まずこのファーストレスポンダに送信されるのです。

そしてそのオブジェクトがイベントを処理できなかった場合、レスポンダチェーンを辿って他のオブジェクトに横流しされます。レスポンダチェーンは、タッチイベントがビュー階層を上層に遡っていったのと同じ繋がり方をしています。

---

InterfaceBuilderのFirstResponderプレースホルダと関連付けた場合、そのメッセージは現在のファーストレスポンダに送信されることになります。

ただしここには2つ注意すべきことがあります。

---

まず第一に、ファーストレスポンダは一つとは限らないということです。

現在のファーストレスポンダを管理しているオブジェクトは何でしょうか?

その答えはウインドウオブジェクトです。Cocoa Touchではプライベートメソッドになっていますが、UIWindowにfirstResponderメッセージを送ると、現在保持しているファーストレスポンダオブジェクトが何かを教えてくれます。(プライベートメソッドはリジェクト対象になるので、ストアに出すアプリでは使わない方がいいです)

これはMac OSXが一つのアプリが複数のウインドウを持っていたためです。ウインドウという概念の乏しいiOSですが、OSXの名残はいくつか見受けられます。

例えばMac OSXでは複数のウインドウのうち、現在ユーザーと対話しているウインドウのことを、「キーウインドウ」と呼びます。iOSではそれを意識することはありませんが、アプリケーション起動時に必ず実行しなければならないUIWindowのメソッドは、makeKeyAndVisible(対象をキーウインドウにして表示せよ)です。

単一のアプリが複数のウインドウを持つOSXのCocoaの流れを汲むため、「個々のウインドウが現在フォーカスしているGUI要素を管理する」という仕組みも引き継いでいます。つまりWindowの数だけファーストレスポンダが存在しているのです。

しかしiOSは一つのUIWindowの下に全てのビューが収まっているため、関係ないように思います。実は違うのです。


問題が起きるのはキーボードを出現させたときです。ソフトキーボードのViewは、UITextEffectsWindow配下となっていて、UIWindowとは独立しているのです。

ゆえに上の画像のようにinputAccessoryViewに閉じるボタンを設置し、そこからInterfaceBuilderのFirstResponderプレースホルダに対してresignFirstResponderメッセージを送るような処理を書いても、UITextEffectsWindowのファーストレスポンダに送信しようとして失敗するため、キーボードは閉じないのです。

---

まったく無駄な知識ですが、UITextEffectsWindowに何らかのテキスト編集要素を持たせることで、UITextEffectsWindowのファーストレスポンダと、UIWindowのファーストレスポンダの、双方が存在する状態にすることが可能です。

その場合、UITextEffectsWindowのファーストレスポンダにresignFirstResponderを送信すると、UIWindowのファーストレスポンダにフォーカスが移ります。


なお「UITextEffectsWindowがファーストレスポンダを持っているときは、UIWindowへのタッチ操作は全て無効になる」という興味深い現象が発生します。

---

そしてもう一つは、もしファーストレスポンダが存在しなかったとき、あるいはファーストレスポンダにイベントを送信したものの、レスポンダチェーンを辿っても処理できなかったときです。

その場合、なぜかヒットテストビューに対してメソッドが実行されるのです。

…そんなことはありえない気がするのですが、実際の挙動を見るとそう考えないと辻褄が合いませんので、そうなのでしょう。

ヒットテストビューに該当するメソッドが実装されていなければ、例によって上層に遡っていき、AppDelegateが処理できなければ、そのメソッドは破棄されます。

---

そもそも、InterfaceBuilderのFirstResponderプレースホルダにメソッドを送信するとはどういうことなのでしょうか?

それを知るためには、まず普通にFile's Ownerのメソッドにbindした時の挙動を知る必要があります。

例えばボタンにメソッドをbindして、ボタンをタップすると、File's Ownerの指定されたメソッドが実行されます。この挙動で「ターゲットとセレクタを保持して、performSelectorを用いているんだろう」と当たりは付くと思いますが、もう少し捻りがあります。

InterfaceBuilderでメソッドとbindするか、もしくはコード上でGUI要素にaddTarget:action:forControlEvents:でアクションと紐付けると、GUI要素のスーパークラスであるUIControlは、内部にアクションとセレクタを保持します。

そしてbindしたイベントと合致するイベント(タップなど)が送信されると、sendAction:to:from:forEvent:を実行します。しかしこのメソッド内部で直接処理を行わず、UIApplicationの同名のメソッドに転送します。そしてUIApplicationからperformSelectorメッセージが送信されるのです。

この、一度UIWindowより上層であるUIApplicationを経由するというのがミソです。

---

ではFirstResponderプレースホルダとbindしたときはどうなるのでしょうか?UIControl内部でセレクタが保持されるのは同じです。しかしこのときは、ターゲットは指定されずnilになっているのです。

なので、FirstResponderプレースホルダへのbindと同じことを、コード上でも実現できます。addTarget:action:forControlEvents:のターゲットをnilにすればいいのです。

あるいは直接、UIApplicationのsendAction:to:from:forEvent:を、to(ターゲット)をnilにすることで、現在のファーストレスポンダに対してメソッドを実行することができます。これをnilターゲットアクションと呼びます。

なぜかStackOverflowを見ても知名度が低いのですが、nilターゲットアクションはファーストレスポンダに対して確実にresignFirstResponderを送信するベストプラクティスでもあります。

---

これでなぜUIControlがターゲットアクションでメソッドを起動しているだけなのに、ウインドウをまたがるとファーストレスポンダへメッセージを送信できないのか?という謎も解けます。

単純なターゲットアクションではなく、sendActionメソッドを通じてUIControlから、ウインドウよりさらに上層のアプリケーションオブジェクトに渡されるので、そこから属しているウインドウのファーストレスポンダを探す処理が入っているのでしょう。

---

まとめ


  • 一般に「ファーストレスポンダ」と呼ばれているものは、各々のウインドウが最初に応答するオブジェクトを保持しているものである。
    • ウインドウオブジェクトは通常の画面を表示するUIWindowと、ソフトキーボードを表示するためのUITextEffectsWindowとがあり、それぞれがファーストレスポンダを管理している。
  • InterfaceBuilderの「FirstResponderプレースホルダ」にbindされたメソッドは、nilターゲットアクションとして実行される。
    • 結果としてもし属しているウインドウにファーストレスポンダが存在すればそこを起点にメソッドが実行される。
    • ファーストレスポンダが存在しない、或いはファーストレスポンダとそのレスポンダチェーンが処理を実行できなかったときは、ヒットテストビューを起点にメソッドが実行される。これはnilターゲットアクションとは関係ない謎現象で、なんでそうなってるのか全然理解できない。
参考文献