私だけなのかもしれないですが、座標を変換するメソッド [UIView convertRect:toView:] がとても苦手でした。座標を変換するだけなはずなのに、いつも引数とレシーバの関係がわからなくなってしまい、総当たりで正しいと思う値を探し出すというエンジニア失格なことをしていました。

これじゃまずい、と思って時間をかけて整理したおかげで、ようやく理解ができました。

もし、自分と同じ悩みを抱えている人がいたら参考にしていただければと思います。

迷っていた原因

私が [UIView convertRect:toView:] で迷っていた原因は、第 1 引数の CGRect を大きく勘違いしていたせいでした。

第 1 引数の CGRect は多くの場合、変換したい座標を持つ View の frame や bounds を指定することが多いと思います。[self convertRect:self.subview.bounds toView:self.superview] のような感じですね。

その場合、登場する View が 3 つ (self, self.subview, self.superview) になるのですが、私は「self.subview.boundsself.superview の座標系に変換するだけだから、レシーバの self は意味なくね?」と思っていました。

よく考えればわかるのですが、第 1 引数の self.subview.bounds には座標系の情報は全くなくて、ただの座標なんですね。第 1 引数に CGRectMake で作った CGRect を指定した場合、座標系の情報なんてないですからね。第 1 引数の CGRect をどのような座標系で示した時のものかは、レシーバが決めるのです。

また、もう一つ迷う要因になっていたのは、framebounds の違いです。framebounds が取り得る値は理解していたのですが、framesuperview の座標系で bounds を表した値 と考えることができていませんでした。

これらの要因で、convertRect が全くわかっていませんでした。では、次に convertRect について解説していきたいと思います。

解説

サンプルコードで説明したいと思います。

メインのビューの上に、redView が、その上に blueView、またその上に yellowView があるというものです。

関係としては、以下のようになります。

self.view
  redView
    blueView
      yellowView
- (void)viewDidLoad {
    [super viewDidLoad];

    UIView *redView = [[UIView alloc] initWithFrame:CGRectMake(50.0, 50.0, 100.0, 100.0)];
    redView.backgroundColor = [UIColor redColor];
    [self.view addSubview:redView];

    UIView *blueView = [[UIView alloc] initWithFrame:CGRectMake(10.0, 10.0, 50.0, 50.0)];
    blueView.backgroundColor = [UIColor blueColor];
    [redView addSubview:blueView];

    UIView *yellowView = [[UIView alloc] initWithFrame:CGRectMake(15.0, 15.0, 30.0, 30.0)];
    yellowView.backgroundColor = [UIColor yellowColor];
    [blueView addSubview:yellowView];

}

さて、yellowView の位置を redView の座標系で表したいと思います。yellowView.frame = (15.0, 15.0, 30.0, 30.0)blueView の座標系で表されたものです。

これを redView の座標系で表すと、(25.0, 25.0, 30.0, 30.0) になります。

この座標を求めるために convertRect:toView: を使います。

yellowView の位置を redView の座標系で表したい場合、[blueView converRect:yellowView.frame toView:blueView.superview] になります。

これを整理すると、

  • blueView ... blueView の座標系で示された
  • yellowView.frame ... yellowView.frame の座標を
  • blueView.superview ... blueView の superview = redView の座標系に変換する。

となります。

また、同じ座標を [yellowView convertRect:yellowView.bounds toView:blueView.superview] でも求めることが出来ます。

これは、

  • yellowView ... yellowView の座標系で示された
  • yellowView.bounds ... yellowView.bounds の座標を
  • blueView.superview ... blueViewsuperview = redView の座標系に変換する。

となります。yellowView.boundsyellowView の座標系で示された yellowView の位置のため、それを redView の座標系に変換することで求めたい座標が取得できます。

さらに、同じ座標を convertRect:fromView でも取得できます。[blueView.superview convertRect:yellowView.bounds fromView:yellowView] とすると同じ座標が返ってきます。

これは、

  • blueView.superview ... blueViewsuperview = redView の座標系に変換します。
  • yellowView.bounds ... yellowView.bounds の座標を
  • yellowView ... yellowView の座標から

となります。メソッドの呼び出し元と第 2 引数が逆になっただけですね。

さいごに

コードだけでわかりづらいですが、なんとなく伝わったでしょうか。私のようなトンチンカンな勘違いをしている人は少ないかもしれませんが、参考になれば幸いです。

参考

self.frameとは[self convertRect:self.bounds toView:self.superView];のシンタックスシュガー