← All topics/UIKit & SwiftUI internals

Question 2

Bounds, frame, and the iOS coordinate system

Explain how bounds and frame differ, what coordinate system each uses, and how center fits in. Why does this matter when positioning subviews or applying transforms?

Follow-ups

  • What happens to frame when you apply a rotation transform?
  • Why does UIScrollView change bounds.origin?

Answer outline

frame and bounds are both rectangles, but they live in different coordinate spaces, and center ties them together:

  1. 1.Frame: a rectangle in the superview's coordinate space. Its origin is where this view's axis-aligned box sits in the parent, so it answers where the view is.
  2. 2.Bounds: a rectangle in the view's own coordinate space, whose size is what drawing, local hit testing, and subview layout use. bounds.origin is usually (0, 0), but UIScrollView moves it through contentOffset to scroll content while its frame in the superview stays fixed.
  3. 3.Center: the view's position in the superview's space, and with bounds it determines frame. Transforms pivot around the anchor point (the center by default), so after a rotation frame grows to the axis-aligned bounding box while bounds keeps the untransformed size.

A subview's frame is always expressed in its superview's space. Mixing up the parent's frame space and the child's bounds space is a common source of misplaced controls and touches that land in the wrong place.

frame lives in the superview's coordinate space and bounds in the view's own space, so after a transform read center and bounds instead of frame.

Principles

  • frame lives in the superview's coordinate space and describes where the view is placed.
  • bounds lives in the view's own coordinate space and is what drawing, hit testing, and subview layout use.
  • After a transform, frame.size no longer has to match bounds.size, because frame becomes the bounding box of the transformed view.
  • Once a transform is set, frame is undefined by contract, so read and write center and bounds instead.

bounds describes local size and scroll offset, and frame describes placement in the superview:

Two rectangles, two spaces
print(view.bounds) // local: size, plus the content offset when scrolling
print(view.frame)  // in the superview: position plus size

Scrolling moves bounds.origin, so different content appears under the same frame in the parent:

Scroll view offset
// contentOffset maps to bounds.origin. The frame in the superview stays put.
scrollView.contentOffset = CGPoint(x: 0, y: 300)
// scrollView.bounds.origin is now (0, 300) and scrollView.frame is unchanged

Follow-up angles

  • CALayer mirrors this with frame, bounds, and position. Its anchorPoint pairs with position to decide where transforms pivot.