Edited at

convertRect:toView:系メソッドについて。

More than 5 years have passed since last update.

iOSではViewの位置を、親のViewの原点からの相対位置で指定し、保持する仕組みになっています。

そのメリットは2点あります。

1.画面の向きを考慮する必要がない

2.親の位置を考慮する必要がない

iPhoneのような画面の向きの自由度の高いモバイル端末では、ユーザーから見た左上の原点座標(0,0)が、物理デバイスの原点座標(0,0)と一致することを保証できません。親の左上原点を(0,0)とした相対座標の仕組みは、この複雑さを隠蔽しています。

またViewが複雑に入れ子状になっているレイアウトを考えると、親の絶対座標を取得/計算する必要がない仕組みはシンプルで便利です。

その反面、ビュー階層内で離れた位置にあるビュー同士の位置関係を参照するには変換処理が必須となります。そのために用意されているのが、convert系の以下の4つのメソッドです。



  • -convertPoint:toView:


  • -convertPoint:fromView:


  • -convertRect:toView:


  • -convertRect:fromView:

しかしこのメソッド群、使うときにレシーバと引数のViewに何を渡せばいいのか混乱してしまったりします。


Viewの座標系ってframe?bounds?

-convertRect:toView: というメソッドのシグネチャから、レシーバのViewの座標系のCGRectを、引数toView:のView座標系に変換するのだ、という当たりは付くと思います。しかしここで、「Viewが持つ座標系」という言葉の意味を勘違いすると、嵌ります。

UIViewは二つのCGRectを持ちます。superviewからの相対位置とサイズを保持したframe、そして自身のviewの原点とサイズを持つboundsです。

UIViewの位置指定はframeで行うため、Viewとboundsよりも、Viewとframeに強い結びつきがあると思い込みがちです。このため、「Viewが持つ座標系の値とは、frameである」という落とし穴に嵌るのです。

位置指定はsuperviewで行うので、frameとはsuperviewの座標系なのであって、「Viewが持つ座標系」とはboundsのことを指すのです。

あるUIView、hogeViewのframe値を、ビュー階層の異なる位置にあるfugaViewのframe値に変換する処理を考えたとき、

[hogeView convertRect:hogeView.frame toView:fugaView];

は間違いで、

[hogeView.superView convertRect:hogeView.frame toView:fugaView];

または

[hogeView convertRect:hogeView.bounds toView:fugaView];

と書かなければなりません。convert~系メソッドで思った値が取得できない場合の原因の9割はここだと思います。Class Referenceをちゃんと読んでいれば、ビューのローカル座標系とはboundsを指すことまで書いてあります。第一にリファレンスに当たるのは大切です。


toView:とfromView:ではレシーバと引数が逆転

convertRectおよびconvertPointでは、fromView:とtoView:という異なるシグネチャを持つ二つのメソッドがあります。変換元と変換先のViewが、レシーバと引数とで逆転します。


convert.m

CGRect frame = CGRectZero;

frame = [self convertRect:self.bounds toView:self.superView];
frame = [self.superView convertRect:self.bounds fromView:self];
frame = [self.superView convertRect:self.frame toView:self.superView];
frame = self.frame;

以上のコードでは全て同じ結果を得られます。

self.frameとは[self convertRect:self.bounds toView:self.superView];のシンタックスシュガーみたいなものである、と覚えておけば、このメソッドで引っかかることもなくなるような気がします。


convertRectの処理は何をやっているのか?

iOSはオープンソースではないので実際にどういう処理が行われているのかは分かりませんが、推測は出来ます。

冒頭の説明の通り、UIViewの座標系では常に親の原点(0,0)が左上になりますが、それは物理デバイスの原点(0,0)と一致するとは限りません。デバイス座標系とビュー座標系の間で相互変換を行っているクラスがいることになります。

その役目を果たすクラスこそ、UIWindowです。

UIWindowのframeは物理デバイスでの座標を保持しています。その原点は左下かもしれませんし、右上かもしれません。しかしsubviewに対して左上原点のビュー座標系を提供することで、物理デバイス座標とビュー座標系の橋渡しを行っているのです。

そしてその逆のことをconvertRectやconvertPointで行っているのでしょう。

親からの相対座標frameでは、ビュー階層の異なる位置にあるViewのframeと直接比較することはできません。そこでまずUIWindow上でのframe値、デバイスの絶対座標に変換するのです。絶対座標同士であれば、相互変換は容易に行えます。

convertRect:toView:convertPoint:toView:で引数のViewにnilを指定した場合、返る値がWindowでの座標系になる、という性質もこれで納得できます。


Windowは一つじゃない

これまではiOSのようなモバイル端末を考えていたので、物理デバイスの座標系とWindowの座標系を同一視してきましたが、これは正しくありません。

OSXのような複数のWindowを持つアプリケーションを考えれば自明なのですが、それぞれのWindowの原点がスクリーン上で異なる位置を持つこともあります。そのような場合、異なるWindowのView同士で座標変換するためにはどうすればいいのでしょうか?

ここではconvertRect:toView:convertPoint:toView:は使えません。UIWindowの次のメソッド群を使います。


  • –convertPoint:toWindow:

  • –convertPoint:fromWindow:

  • –convertRect:toWindow:

  • –convertRect:fromWindow:

これらのメソッドは、それぞれのWindowが持つ、Window座標系の値を、上位の(今度こそ本物の)物理デバイスの論理座標系で比較します。


座標系階層の整理

ここまでの座標系の階層を整理します。


1.物理デバイス論理座標系

物理デバイスの各ピクセルをOSが扱いやすいように写像した座標系です。アプリケーションプログラマ視点ではたぶん最上の座標系です。

retinaディスプレイのように、物理デバイス論理座標系は実際の物理ピクセルと1対1対応していないので、実際にはさらに上位の座標系も存在しています。


2.Window座標系

複数のウインドウを持つ(特にデスクトップの)アプリケーションの各々のウインドウがもつ座標系です。スクリーン上での絶対座標を保持していて、画面の向きの自由度が高いモバイルデバイスではその複雑さを吸収する役割も果たしています。

ウインドウをまたいで座標変換する必要がある場合は、Window座標系の値を上位の物理デバイス論理座標系を経由して変換します。


3.View座標系

左上を原点(0,0)として、子ビューの位置を管理する、アプリケーションプログラマにとって一番馴染みのある座標系です。

ビューをまたいで座標変換する必要がある場合、同一Window内であれば、convertRect:toView:などのメソッドを用いて、一度Window座標系を経由することで他のView座標系の値へと変換することができます。

異なるWindowのView座標系の値へと変換する場合は、Window座標系の値に変換し、その値を物理デバイス論理座標系を経由して別のWindow座標系に渡す…という二手間掛ける必要があります。


余談。なぜUIKeyboardのNotificationの値はWindow座標系なのか?

UIKeyboardの各種Notificationを使われたことのある方は、userInfoの中に格納されたソフトウェアキーボード座標がView座標系ではなくWindow座標系の値であることをご存知だと思います。

キーボードの出現/消去の通知で渡されるソフトウェアキーボードの位置や大きさは、画面の向きによってはoriginやsizeの値が反転しているので、その判定処理を書かなければならないのです。

それを不思議に思われた方もいるのではないでしょうか。

しかしここまでの内容を理解して、かつiOSのソフトウェアキーボードが、通常のアプリケーションのView階層が持つWindowとは別の、UITextEffectsWindowというprivateクラスのWindowに属している、ということに気付けば納得できると思います。

つまり、ソフトウェアキーボードのframeを、アプリケーション側のWindowに渡すためには、


convert2.m

CGRect windowFrame = [inputView convertRect:inputView.bounds toView:nil]];

CGRect result = [inputView.window convertRect:windowFrame toWindow:[[UIApplication sharedApplication] keyWindow]];

という二段階の座標変換を行わなければならないのです。

この結果は論理デバイス座標系のレイヤを経由しなければならないので、Window座標系の値になるのです。UIKeyboardNotificationに関する定数が、UIWindowのヘッダに記載されているのも納得がいきます。

こういうもやもやとした約束事の意味が分かると、ものすごくスッキリします。