Subviews of UIStackView don't return expected frame origins with respect to superview

Hi all,


I'm sure I'm doing something wrong with UIView.convert(), so maybe you can set me on the right path here. I'm working in Swift 3.


For simplicity's sake, let's say I have a UIViewController which contains nothing more than a UIStackView with two UIView subviews, leftView and rightView, side by side. Assume leftView and rightView are exactly the same size—let's say 200 by 200 points.


I want to get the origin of these subviews with respect to the view controller's origin. In other words, in my view controller, I call


let leftViewOrigin: CGPoint = leftView.convert(leftView.frame.origin, to: self.view)


And I get the point I expect for leftView, some horizontal and vertical offset—say (16, 100)—from the view controller's view's origin. Great!


If I then do the same to get the origin of the rightView:


let rightViewOrigin: CGPoint = rightView.convert(rightView.frame.origin, to: self.view)


I should get the same CGPoint as I did for leftView, plus a horizontal offset equal to the width of leftView, or (216, 100)—216 because 16 from leftViewOrigin.x plus the 200 point width of leftView. In other words, I expect to get the point corresponding to the top-left corner of rightView.


Instead, I get the point corresponding to the top-right corner of rightView—in this example, (416, 100), where 416 is the 16 from leftViewOrigin.x, plus the 200-point width of leftView, plus the 200-point width of rightView.


I've put a sample project up on GitHub to show this behaviour.


For the record, I see the same behaviour regardless of whether or not I use convert(to:) or convert(from:), and (assuming this is the main, full-screen, view controller), whether I convert to self.view or nil.


On the off chance that I'm doing this correctly and there's a bug, I will file a radar.

Answered by Frameworks Engineer in 177562022

The point you pass needs to be in the coordinate system of the view that you are requesting do the conversion. Your code is requesting the frame.origin of rightView as if rightView were a subview of itself.


The simplest fix is to change <view>.frame.origin in your code to <view>.bounds.origin. This "works" for leftView because leftView.frame.origin is likely the same value as leftView.bounds.origin.

Accepted Answer

The point you pass needs to be in the coordinate system of the view that you are requesting do the conversion. Your code is requesting the frame.origin of rightView as if rightView were a subview of itself.


The simplest fix is to change <view>.frame.origin in your code to <view>.bounds.origin. This "works" for leftView because leftView.frame.origin is likely the same value as leftView.bounds.origin.

Thanks! Your fix returns the values I'd expect—I need to dig a bit deeper into frames vs bounds in the view hierarchy for my own understanding. 🙂


One thing that still confuses me is why calling


let rightViewOrigin: CGPoint = rightView.convert(rightView.frame.origin, to: nil)


doesn't work as expected. Per the reference documentation, shouldn't that always return frame.origin of rightView with respect to the window base coordinates?

It does do the conversion with respect to rightView.window, but again your doing the view's *frame* origin in that view's own coordinate system.


The way to consider frames and bounds is pretty simple – bounds defines a view's own coordinate system. If you draw or position subviews, you do so in a view's bounds. The frame is the view in its parent's coordinate system. When you layout subviews you set their frame to express their position in that view's coordinate system.

Subviews of UIStackView don't return expected frame origins with respect to superview
 
 
Q