iosNudge

What is the difference between frame and bounds?

EasyAsked oftenUIKit 2 min read

TL;DR

frame is where the view sits inside its superview. bounds is the view's rectangle in its own coordinate space. Its origin is usually (0, 0). Use frame to place a view. Use bounds to draw or lay out things inside it.

A simple example

let parent = UIView(frame: CGRect(x: 0, y: 0, width: 300, height: 300))
let child = UIView(frame: CGRect(x: 50, y: 100, width: 120, height: 80))
parent.addSubview(child)

print(child.frame)  // (50.0, 100.0, 120.0, 80.0)  -> position in parent
print(child.bounds) // (0.0, 0.0, 120.0, 80.0)     -> its own space
print(child.center) // (110.0, 140.0)              -> also in parent space
PropertyCoordinate spaceTypical use
frameSuperviewPosition and size a view
boundsThe view itselfLay out subviews, draw content
centerSuperviewMove a view, works well with transforms

Transforms change the frame, not the bounds

When you rotate or scale a view, bounds stays the same. The frame becomes the smallest box that contains the rotated view.

child.transform = CGAffineTransform(rotationAngle: .pi / 4)
print(child.bounds) // still (0, 0, 120, 80)
print(child.frame)  // bigger box, around 141 x 141

Apple says frame is undefined when the transform is not identity. So with transforms, use bounds and center, not frame.

Changing bounds.origin scrolls the content

bounds.origin says which part of the view's own space is visible. If you move it, all subviews move the other way. This is exactly how UIScrollView works.

let scrollView = UIScrollView()
scrollView.contentOffset = CGPoint(x: 0, y: 200)
print(scrollView.bounds.origin) // (0, 200)

So contentOffset is just bounds.origin.

Converting between spaces

To compare points from different views, convert them first.

let pointInWindow = child.convert(CGPoint.zero, to: nil) // nil = window
let rectInParent = child.convert(child.bounds, to: parent) // equals child.frame

Which one to use in layoutSubviews

Lay out children using your own bounds. Your frame is in your parent's space, so it is the wrong reference.

override func layoutSubviews() {
    super.layoutSubviews()
    iconView.frame = CGRect(x: 0, y: 0, width: bounds.height, height: bounds.height)
}

Common mistakes

  • Using self.frame to lay out subviews. If the view is not at (0, 0) in its parent, everything shifts.
  • Reading frame after a rotation transform.
  • Reading sizes in viewDidLoad. Auto Layout has not run yet.

Common follow-up questions

  • When can bounds.origin be non-zero? In scroll views, or when you set it yourself to shift content.
  • Can frame and bounds sizes differ? Yes, when the view has a scale or rotation transform.
  • What does center use? The superview's coordinate space, like frame.
  • How do you get a view's position on screen? Use convert(_:to: nil) to get window coordinates.
  • Does SwiftUI have frame and bounds? It has .frame() for size and GeometryReader or onGeometryChange to read size and coordinate spaces.