What is the difference between frame and bounds, and what happens to them under a transform?
Predict first
What do frame and bounds report after a 2× scale?
import UIKit
let t = UIView(frame: CGRect(x: 0, y: 0, width: 200, height: 100))
t.transform = CGAffineTransform(scaleX: 2, y: 2)
print(t.frame, t.bounds)
- (0,0,400,200) (0,0,200,100)
- (-100,-50,400,200) (0,0,200,100)
- (-100,-50,400,200) (-100,-50,400,200)
Predict first
Where is the subview drawn after the parent’s bounds.origin.x is set to 50?
import UIKit
let root = UIView(frame: CGRect(x: 0, y: 0, width: 300, height: 300))
let p = UIView(frame: CGRect(x: 0, y: 0, width: 100, height: 100))
root.addSubview(p)
let s = UIView(frame: CGRect(x: 10, y: 10, width: 20, height: 20))
p.addSubview(s)
p.bounds.origin = CGPoint(x: 50, y: 0)
print(s.convert(CGPoint.zero, to: root), s.frame.origin)
- (10, 10) (10, 10)
- (-40, 10) (10, 10)
- (60, 10) (60, 10)
Mental model. The frame is where a picture hangs on the wall. The bounds is the picture’s own coordinate grid: shifting the grid moves what you see inside the frame without moving the frame.
frame is the view’s rectangle in its superview’s coordinate system: where it sits. bounds is its rectangle in its own coordinate system: the size and the origin of its content. center is the midpoint of the frame in the superview. They are linked: changing bounds.size or center changes frame. A freshly created view has bounds.origin at zero, so its subviews are positioned from its top-left corner.
Two behaviours follow. First, a transform changes the frame but not the bounds: a view with a 2× scale has the same bounds but a frame twice as large, centred on the same point (and with a rotation, the frame becomes the bounding box of the rotated shape, so reading or setting it is meaningless). Use center and bounds when a transform is applied. Second, changing bounds.origin moves the content without moving the view: the subviews’ own frames do not change, but they are drawn shifted. That is precisely how UIScrollView scrolls: contentOffset is its bounds.origin. Convert between systems with convert(_:to:) and convert(_:from:).
The view is scaled about its centre, so its frame grows in both directions, while its own bounds are unchanged.
The subview’s own frame is unchanged, but the parent’s content is shifted left by 50, so it appears at x = −40 in the root’s coordinates. This is how scrolling works.
Common traps
- Setting
frameon a transformed view. The result is undefined. Setcenterandboundsinstead. - Using a subview’s frame as if it were in window coordinates. It is relative to its superview; use
convert.
They might follow up with
- How does a scroll view scroll? It changes its own
bounds.origin, so every subview is displaced without being touched. - Why does a rotated view have a big frame? The frame is the smallest axis-aligned rectangle that contains the rotated view.