What is the difference between frame and bounds, and what happens to them under a transform?

7.2.2 · L1 · Interview

Reported at: Spotify

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 frame on a transformed view. The result is undefined. Set center and bounds instead.
  • 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.

Related questions

  • 7.2.1 What is the relationship between UIView and CALayer?
  • 7.1.1 How does UIKit decide which view receives a touch, and why can a subview outside its parent’s bounds not be…
  • 6.3.1 What is a run loop, and what happens in one iteration?

← What is the relationship between UIView and CALayer?