2012年8月22日水曜日

組み込みコンテナビューコントローラをカスタムコンテナビューコントローラに組み込むときの落とし穴

iOS3まではrootViewControllerの概念がなかったので、windowに対して直接addSubviewしていた。このときwindowのboundsから、ステータスバーの長さ20ポイント分だけoriginとheightを操作する必要があった。

iOS4以降はrootViewControllerに指定したViewControllerのviewは、ステータスバーの長さを省いたwindowの大きさに、自動的に調整されるようになった。

---

本来、最も画面の外側(すなわちコンテナの役割を持つ)ViewControllerのViewは、インターフェース要素を加味して、自身の位置を調整する必要があったのだ。

しかも厄介なのは、子viewは親viewのoriginからの相対位置であるローカル座標系で計算するのに対して、最外周ではUKit論理座標系で計算しなければならないということにある。UKit論理座標とは、(画面の向きに関わらず)スクリーン左上が原点(0,0)となる座標系のことである。

例えばビューを平行移動したいとき、ローカル座標系でframeのwidthプロパティを操作すると、デバイスの向きに関わらず左右移動になるのに対して、UKit論理座標系のframeを持つwindowのルートビューのwidthを操作すると、ポートレートでは左右移動になるが、ランドスケープでは上下移動になってしまう。

逆に言えば、ローカル座標系のおかげで画面の向きによってwidthとheightの意味が逆になるといった煩わしい問題から解放されているのである。

(現在はやるべきではないがwindowに対してaddSubviewするときに、frameではなくboundsを使ったのも、frameはUKit論理座標系なのに対して、boundsはwindowのローカル座標系になるので、向きを考慮する必要がなくなるためである)

---

この問題が組み込みビューコンテナビューコントローラとどう繋がるのかと言うと。

NavigationControllerやTabBarControllerは最外周のコンテナとして振舞うことを前提に作られているので、UKit論理座標系として自身のインターフェース要素分のマージンを考慮したframe値を設定しようとするのである。

そのため、組み込みビューコンテナを自作コンテナの子にしてしまうと、ローカル座標系に対してUKit座標系を想定した操作を行うために、以下のような現象が発生する。

  • rootViewControllerがステータスバーのマージンを設定しているにも関わらず、追加で不要なマージンを設定しようとする。
  • 実際の画面の向きと異なる方向にマージンを設定しようとする。
  • widthとheightの意味を取り違えて、ランドスケープモードにも関わらず、縦長のsizeを設定しようとする。

もっとも、実際にはrootViewControllerの特性によって、rootViewControllerのviewプロパティは、windowを満たすようにリサイズされてしまうせいなのか、顕在しにくいですが。

---

簡単に問題に遭遇するならば、自作コンテナViewControllerを作成し、組み込みコンテナビューコントローラを対象にtransitionFromViewController:toViewController:duration:options:animations:completion:で、トランジションではなく、カスタムUIViewアニメーションを実行すると、rootViewControllerの補完が働く前にどのようなframe値が設定されるのか一目瞭然となります。

またその対策として最も簡単と思われる方法は、transitionFromViewController以下略メソッドを封印して、組み込みコンテナビューコントローラのviewを直接addSubviewした上で、正しいframe値を与えることです。正しいframe値の取得は、rootViewControllerのviewのself.view.boundsを使えば問題ないはずです。

---

コンテナviewControllerは自身のviewの位置を調整する責務があり、どこかでviewの位置を調整する処理を行っています。考えられる場所はviewWill(Did)Appearや、iOS5以降ではviewWillLayoutSubviewsあたりです。

上の解決法は、

  • 直接addSubviewを行った場合、viewWill(Did)Appearが呼ばれない。
  • addSubviewによってviewWill(Did)Appearによる調整が行われた後で、frame値を変更しているため、view調整を上書きしている。
のいずれかの現象が起きているのだと当たりをつけて、実証コード(NavigationControlelrのプロトコルでコールバックメソッドをフックしてプロパティを参照)を書いてみたところ、viewWillAppearの時点ではUKit座標系の間違った情報を持っていて、viewDidAppearで修正されていました。

コールバックメソッドが呼ばれていることから1の可能性はなく(iOS5以降はほぼこれらのメソッドはコールされるようです。バグで複数回コールされることも)、Didの時点でframe値の修正が行われていることから、2の可能性も否定されました。

どういうことなんだろう。もうちょっと調査してみるかもしれず。

0 件のコメント:

コメントを投稿