- Published on
- 4 min read
> The Difference Between Frame and Bounds in UIKit
Every UIView has two properties that describe its size and position: frame and bounds. They look similar but serve different purposes, and confusing them leads to layout bugs that are hard to track down.
Frame: Position in the Parent
The frame describes where the view sits within its superview's coordinate system. It's the rectangle that contains the view from the parent's perspective:
let redView = UIView()
redView.backgroundColor = .red
redView.frame = CGRect(x: 50, y: 100, width: 200, height: 150)
view.addSubview(redView)
This places a red view 50 points from the left edge and 100 points from the top of its parent view. The frame's origin (x, y) is relative to the superview.
Bounds: The View's Own Coordinate System
The bounds describes the view's internal coordinate system. Its origin is typically (0, 0), and its size matches the view's dimensions:
let redView = UIView(frame: CGRect(x: 50, y: 100, width: 200, height: 150))
print(redView.frame) // (50.0, 100.0, 200.0, 150.0)
print(redView.bounds) // (0.0, 0.0, 200.0, 150.0)
The bounds size equals the frame size, but the bounds origin stays at (0, 0) by default. This is the coordinate system used when positioning subviews inside this view.
Why Bounds Exists
When you add a subview, its frame is relative to the parent's bounds, not the parent's frame:
let parentView = UIView(frame: CGRect(x: 100, y: 100, width: 300, height: 300))
parentView.backgroundColor = .blue
let childView = UIView(frame: CGRect(x: 10, y: 10, width: 50, height: 50))
childView.backgroundColor = .yellow
parentView.addSubview(childView)
The child sits 10 points from the top-left of the parent's bounds, not from (100, 100) on screen. The parent's frame position is irrelevant to its children.
When Frame Changes
Transforms affect the frame but not the bounds. If you rotate a view, its frame becomes the axis-aligned bounding box that contains the rotated view:
let square = UIView(frame: CGRect(x: 100, y: 100, width: 100, height: 100))
square.backgroundColor = .green
view.addSubview(square)
print("Before rotation:")
print("Frame: \(square.frame)") // (100.0, 100.0, 100.0, 100.0)
print("Bounds: \(square.bounds)") // (0.0, 0.0, 100.0, 100.0)
square.transform = CGAffineTransform(rotationAngle: .pi / 4)
print("After 45-degree rotation:")
print("Frame: \(square.frame)") // (~79.3, ~79.3, ~141.4, ~141.4)
print("Bounds: \(square.bounds)") // (0.0, 0.0, 100.0, 100.0)
The bounds stay the same because the view's internal coordinate system hasn't changed. The frame grows to contain the rotated square. This is why you shouldn't use frame to set a view's size after applying transforms.
Scrolling with Bounds
UIScrollView uses bounds.origin to implement scrolling. When you scroll, the scroll view changes its bounds origin:
// Scrolling down by 100 points
scrollView.bounds.origin.y = 100
// Same as:
scrollView.contentOffset = CGPoint(x: 0, y: 100)
The subviews don't move in the superview's coordinate system. Instead, the visible window (the bounds) shifts over the content. This is why changing bounds.origin feels like scrolling.
Practical Guidelines
Use frame when positioning a view inside its parent:
let button = UIButton(type: .system)
button.setTitle("Tap Me", for: .normal)
button.sizeToFit()
button.frame.origin = CGPoint(x: 20, y: 100)
containerView.addSubview(button)
Use bounds when working with a view's internal coordinate system:
let label = UILabel()
label.text = "Centered"
label.sizeToFit()
label.center = CGPoint(
x: containerView.bounds.midX,
y: containerView.bounds.midY
)
containerView.addSubview(label)
Use bounds when drawing:
override func draw(_ rect: CGRect) {
let path = UIBezierPath(ovalIn: bounds)
UIColor.blue.setFill()
path.fill()
}
Don't rely on frame after applying transforms. Use bounds.size and center instead:
view.transform = CGAffineTransform(scaleX: 2, y: 2)
// Wrong: frame is now larger than the original size
let incorrectSize = view.frame.size
// Right: bounds still reflects the original size
let correctSize = view.bounds.size
Common Confusion
Setting a view's frame also sets its bounds.size and center:
view.frame = CGRect(x: 50, y: 100, width: 200, height: 150)
// Equivalent to:
view.bounds.size = CGSize(width: 200, height: 150)
view.center = CGPoint(x: 150, y: 175) // 50 + 200/2, 100 + 150/2
But this only works cleanly when there's no transform applied.
Sample Project
Want to see this code in action? Check out the complete sample project on GitHub:
The repository includes a working Xcode project with all the examples from this article, plus unit tests you can run to verify the behavior.
Summary
Frame is for positioning within a parent. Bounds is the view's own coordinate system. The frame can change based on transforms while bounds stays stable. When laying out subviews, always use the parent's bounds to calculate positions, not its frame.
// Continue_Learning
Finding the Top View Controller in Swift
How to traverse the view controller hierarchy to find the currently visible view controller.
StoreKit 2 for In-App Purchases and Subscriptions
A practical guide to implementing in-app purchases and subscriptions with StoreKit 2, covering products, transactions, subscription status, and SwiftUI integration.
Scheduling Alarms with AlarmKit
Learn how to use AlarmKit to schedule alarms and countdown timers that appear on the Lock Screen and Dynamic Island on iOS 26.
// Stay Updated
Get notified when I publish new tutorials on Swift, SwiftUI, and iOS development. No spam, unsubscribe anytime.