Swiftyn LogoSwiftyn
LearnInterview PrepRoadmapsArchitect Profile
Swift LanguageSwiftUIUIKitiOS ConceptsmacOS

UIKit Topics

Introduction to UIKitUIKit App LifecycleUIApplicationAppDelegateSceneDelegateUIViewUIViewControllerUIViewController LifecycleResponder ChainUIWindowUILabelUIButtonUIImageViewUITextFieldUITextViewUISwitchUISliderUIStepperUISegmentedControlUIProgressViewUIActivityIndicatorViewUIDatePickerUIPickerViewFrames and BoundsAuto Layout
Browse UIKit Topics
Introduction to UIKitUIKit App LifecycleUIApplicationAppDelegateSceneDelegateUIViewUIViewControllerUIViewController LifecycleResponder ChainUIWindowUILabelUIButtonUIImageViewUITextFieldUITextViewUISwitchUISliderUIStepperUISegmentedControlUIProgressViewUIActivityIndicatorViewUIDatePickerUIPickerViewFrames and BoundsAuto Layout
Swiftyn Logo

Swiftyn

The go-to platform for Apple developers. Swift, SwiftUI, and beyond.

Questions? Email us at support@swe180.com

Categories

  • SwiftUI
  • Swift Language
  • Xcode
  • visionOS

Our Products

  • SWE180
  • One Percent Engineer

Resources

  • About
  • RSS Feed
  • Apple Developer

© 2026 Swiftyn. All rights reserved.

Privacy PolicyTerms of Service

Swiftyn is the premier learning platform and developer resource for mastering the Apple ecosystem. Whether you are an aspiring iOS developer looking to learn Swift 6, a macOS engineer diving into advanced system architecture, or an XR pioneer building the future with visionOS, our beautifully crafted tutorials, roadmaps, and interview prep guides have you covered. Built by Apple developers, for Apple developers.

UIKit10 min read

Mastering UIView's `frame` and `bounds` in iOS Development

Understanding the difference between `frame` and `bounds` is fundamental for any iOS developer working with `UIView`s. These two properties, while seemingly similar, represent distinct concepts in a view's geometry and coordinate system. Master them to achieve precise and predictable layouts.

Introduction to UIView Geometry

In UIKit, every UIView has a defined position and size within its superview's coordinate system, as well as an internal coordinate system for its subviews. Understanding how frame and bounds relate to these coordinate systems is crucial for layout management, custom drawing, and touch event handling. Many developers initially find these concepts confusing, but with a clear explanation, you'll see they serve distinct and important purposes.

Before diving into the specifics, let's establish a common understanding of coordinate systems in iOS. The origin (0,0) for any view's coordinate system is typically at its top-left corner. The x-axis extends to the right, and the y-axis extends downwards. This fundamental principle applies consistently across frame and bounds with subtle but significant differences in their reference points.

Understanding frame

The frame property of a UIView is a CGRect that defines the view's size and position relative to its superview's coordinate system. Think of frame as the external representation of a view – how it looks to its parent.

When you set a view's frame, you're telling its superview where to place it and how large it should be. The origin of the frame is the top-left corner of the view, positioned within the superview's coordinate space. The size of the frame represents the view's external dimensions.

Changing a view's frame affects its position and size as seen by its superview. Consequently, this also influences the layout of any siblings or its superview itself if it's using layout constraints based on its subviews. It is the primary property you'll interact with when positioning views manually within a superview.

Key Characteristics of frame:

  • Reference System: Superview's coordinate system.
  • Origin: Top-left corner of the view, relative to its superview.
  • Use Cases: Positioning and sizing a view within its parent, animation of position/size.
  • Rotation/Scaling Impact: When a view's transform property is modified (e.g., for rotation, scaling, or translation), the frame becomes undefined if the transform is not the identity transform. In such cases, you should rely on bounds and center instead. The CALayer's frame property (which UIView wraps) is actually a derived property and can become invalid if the view is rotated.

Let's look at an example. Suppose you have a parentView and you want to place a childView at a specific position within parentView's bounds.

swift
import UIKit

func demonstrateFrame() {
    let parentView = UIView(frame: CGRect(x: 0, y: 0, width: 300, height: 300))
    parentView.backgroundColor = .lightGray

    let childView = UIView()
    childView.backgroundColor = .systemBlue

    // Set the childView's frame relative to its superview (parentView)
    childView.frame = CGRect(x: 50, y: 50, width: 100, height: 100)

    parentView.addSubview(childView)

    print("Parent View Frame: \(parentView.frame)")     // X: 0, Y: 0, W: 300, H: 300
    print("Child View Frame: \(childView.frame)")      // X: 50, Y: 50, W: 100, H: 100
    print("Child View Bounds: \(childView.bounds)")     // X: 0, Y: 0, W: 100, H: 100

    // Changing childView's frame moves it within parentView
    childView.frame.origin.x += 20
    print("Child View Frame after origin change: \(childView.frame)") // X: 70, Y: 50, W: 100, H: 100

    // Changing childView's frame size resizes it within parentView
    childView.frame.size.width += 30
    print("Child View Frame after size change: \(childView.frame)")  // X: 70, Y: 50, W: 130, H: 100
}

demonstrateFrame()

Understanding bounds

The bounds property of a UIView is also a CGRect, but it defines the view's size and position relative to its own coordinate system. Think of bounds as the internal representation of a view – how it sees itself and how its subviews are positioned within it.

By default, a view's bounds.origin is (0,0), and its bounds.size is equivalent to its frame.size. However, you can change the bounds.origin to shift the content within the view's visible area, effectively scrolling its internal content without moving the view itself within its superview. This is a powerful mechanism used, for example, by UIScrollView to present content larger than its visible frame.

Changing a view's bounds only affects its internal drawing and the positioning of its subviews. It does not move the view within its superview's coordinate system. If you change bounds.size, the view will implicitly resize (and thus its frame.size will also change), but if you change bounds.origin, the view's current frame and center properties remain unchanged.

Key Characteristics of bounds:

  • Reference System: View's own local coordinate system.
  • Origin: Top-left corner of the view's internal content area, relative to itself.
  • Use Cases: Custom drawing (drawing always happens in bounds coordinates), positioning subviews within the parent view, implementing scrolling mechanisms (by changing bounds.origin).
  • Rotation/Scaling Impact: Unlike frame, bounds.size accurately reflects the view's dimensions after transformations have been applied, making it more reliable for drawing and internal layout when transforms are involved.

Consider the same parentView and childView. If you modify childView.bounds.origin, childView itself doesn't move relative to parentView, but its subviews (or custom drawing) within childView would shift.

swift
import UIKit

func demonstrateBounds() {
    let parentView = UIView(frame: CGRect(x: 0, y: 0, width: 300, height: 300))
    parentView.backgroundColor = .lightGray

    let containerView = UIView(frame: CGRect(x: 50, y: 50, width: 200, height: 200))
    containerView.backgroundColor = .systemMint.withAlphaComponent(0.5)
    parentView.addSubview(containerView)

    let innerContent = UIView(frame: CGRect(x: 0, y: 0, width: 100, height: 100))
    innerContent.backgroundColor = .systemOrange
    containerView.addSubview(innerContent)

    print("\n--- Initial State ---")
    print("Container Frame: \(containerView.frame)") // X: 50, Y: 50, W: 200, H: 200
    print("Container Bounds: \(containerView.bounds)") // X: 0, Y: 0, W: 200, H: 200
    print("Inner Content Frame (relative to container): \(innerContent.frame)") // X: 0, Y: 0, W: 100, H: 100

    // Change the bounds.origin of the containerView
    // This shifts the internal coordinate system of containerView
    // Its subviews (like innerContent) will appear to move *within* containerView
    // but containerView itself does not move relative to parentView.
    containerView.bounds.origin.x += 20
    containerView.bounds.origin.y += 20

    print("\n--- After changing containerView.bounds.origin ---")
    print("Container Frame: \(containerView.frame)") // Still X: 50, Y: 50, W: 200, H: 200 (frame not changed!)
    print("Container Bounds: \(containerView.bounds)") // Now X: 20, Y: 20, W: 200, H: 200
    
    // The innerContent's frame is still (0,0,100,100) relative to its parent's (containerView's) *new* coordinate system origin.
    // Visually, it would appear to have moved effectively by (-20,-20) relative to the containerView's visible area.
    print("Inner Content Frame (relative to container): \(innerContent.frame)") // X: 0, Y: 0, W: 100, H: 100

    // Changing bounds.size *does* change the frame.size implicitly
    containerView.bounds.size.width += 50
    print("\n--- After changing containerView.bounds.size ---")
    print("Container Frame: \(containerView.frame)") // Frame width will now be 250
    print("Container Bounds: \(containerView.bounds)") // Bounds width will now be 250
}

demonstrateBounds()

Coordinate System Transformation and center

It's important to understand how frame and bounds interact with coordinate system transformations, especially through the center property and transform property.

The center property is a CGPoint that defines the center of the view relative to its superview's coordinate system. It's a convenient way to position a view without directly manipulating its frame.origin. The center always remains valid even if a view's transform property causes its frame to become undefined.

When you apply a CGAffineTransform to a view's transform property (e.g., rotation, scaling), the bounds of the view reflect its untransformed size, while its frame changes to encompass the transformed view within its superview's coordinate space. If the transformation involves rotation, the frame will grow to accommodate the new rotated bounding box, and its origin will shift. If a view is rotated such that its alignment with the axes changes, its frame effectively becomes undefined; you should refer to center and bounds for geometry information.

Practical Example: Rotating a View

Consider rotating a square view. Its bounds.size will remain (100, 100) (assuming it started as a 100x100 square), because bounds describes the internal content area, which doesn't change intrinsic size when rotated. However, its frame.size will expand to (sqrt(2)*100, sqrt(2)*100) (approximately 141x141) after a 45-degree rotation, because the frame must be an axis-aligned bounding box of the transformed view.

swift
import UIKit

func demonstrateTransformImpact() {
    let superview = UIView(frame: CGRect(x: 0, y: 0, width: 400, height: 400))
    superview.backgroundColor = .darkGray

    let rotatingView = UIView(frame: CGRect(x: 100, y: 100, width: 100, height: 100))
    rotatingView.backgroundColor = .systemRed
    superview.addSubview(rotatingView)

    print("\n--- Before Transform ---")
    print("Rotating View Frame: \(rotatingView.frame)")   // X: 100, Y: 100, W: 100, H: 100
    print("Rotating View Bounds: \(rotatingView.bounds)") // X: 0, Y: 0, W: 100, H: 100
    print("Rotating View Center: \(rotatingView.center)") // X: 150, Y: 150 (since 100+100/2, 100+100/2)

    // Apply a 45-degree rotation transform
    rotatingView.transform = CGAffineTransform(rotationAngle: .pi / 4) // 45 degrees

    print("\n--- After 45-degree Rotation ---")
    // Frame changes to encompass the rotated view. Its size increases.
    print("Rotating View Frame: \(String(format: "X: %.2f, Y: %.2f, W: %.2f, H: %.2f", mutating: rotatingView.frame.origin.x, rotatingView.frame.origin.y, rotatingView.frame.size.width, rotatingView.frame.size.height))")
    // Example output might be: X: 129.29, Y: 129.29, W: 141.42, H: 141.42
    
    // Bounds remain unchanged because it refers to the internal logical size
    print("Rotating View Bounds: \(rotatingView.bounds)") // X: 0, Y: 0, W: 100, H: 100 (still!) 

    // Center remains unchanged relative to superview, as rotation is around the center
    print("Rotating View Center: \(rotatingView.center)") // X: 150, Y: 150 (still!)

    // Reset transform and then scale
    rotatingView.transform = .identity
    rotatingView.transform = CGAffineTransform(scaleX: 0.5, y: 0.5)

    print("\n--- After Scaling (0.5x) ---")
    // Frame shrinks due to scaling
    print("Rotating View Frame: \(rotatingView.frame)")   // X: 125, Y: 125, W: 50, H: 50
    // Bounds size changes with scaling because bounds *represents* the active logical size
    // This is subtle: `bounds.size` will reflect the scaled size only if the transform effectively changes the scale.
    // For a generic transform, `bounds` gives the untransformed internal coordinates.
    // However, when *only* scaling, for convenience, `bounds.size` often reflects the scaled dimensions.
    // It's safer to always think of `bounds` as the internal coordinate space which usually starts at (0,0) with its *intrinsic* size.
    // In UIKit, `bounds.size` *does* change when the `transform` property modifies the scale, as it implicitly resizes the view.
    print("Rotating View Bounds: \(rotatingView.bounds)") // X: 0, Y: 0, W: 50, H: 50
    
    print("Rotating View Center: \(rotatingView.center)") // X: 150, Y: 150 (still!)
    
    // iOS compatibility: These behaviors are consistent across all modern iOS versions (iOS 9+).
}

demonstrateTransformImpact()

When to use frame vs. bounds?

Choosing between frame and bounds hinges on what you intend to do:

  • Use frame when you want to:

    • Position a view within its superview.
    • Set the external size of a view.
    • Animate a view's position or size without applying complex transforms.
    • Convert points or rectangles between a view and its superview.
  • Use bounds when you want to:

    • Perform custom drawing within the view (e.g., in draw(_:) method, the CGContext operates in bounds coordinates where (0,0) is the top-left of the view's internal space).
    • Position subviews relative to the current view's origin.
    • Implement a scrolling mechanism by changing bounds.origin.
    • Query the internal dimensions of a view, especially after complex transformations (where frame might be problematic).
    • Convert points or rectangles between a view and its subviews.

Remember that frame and bounds are intrinsically linked. Changing frame.size will change bounds.size, and vice versa. Changing bounds.origin will not change frame.origin or center, but it will shift the internal content. Changing frame.origin will shift the view and all its content and subviews, thus changing center but leaving bounds.origin as (0,0) (unless a custom bounds origin was already set).

If you find yourself manually setting frames and bounds in modern iOS development, consider leveraging Auto Layout, which abstracts away much of this manual coordinate system management. However, even with Auto Layout, understanding frame and bounds provides a deeper understanding of how your UI is rendered (internally, Auto Layout resolves to frames and bounds during the layout pass).

Frame and Bounds are Interchangeable

Becoming a stronger iOS Engineer

THE MYTH or PROBLEM: Frame and Bounds are Interchangeable

Many new iOS developers mistakenly believe that `frame` and `bounds` can be used interchangeably, or that their differences are negligible. This leads to unexpected layout issues, incorrect drawing, and problems with touch event handling, especially when transformations are involved.

swift
let myView = UIView(frame: CGRect(x: 10, y: 10, width: 100, height: 100))
// Later...
myView.bounds.origin = CGPoint(x: 50, y: 50) // What happens internally?

WHAT HAPPENS INTERNALLY? Coordinate System Shifts

When you modify `frame` or `bounds`, you are manipulating different aspects of the view's geometry. `frame` defines the view's external box in the superview's space. `bounds` defines the view's internal box in its own local space, which affects how it presents itself and positions subviews.

UIWindow (Root Coordinate System)
Super View (Its own Bounds)
- Sub View A (Frame relative to Super, Bounds internal)
- Sub View B (Frame relative to Super, Bounds internal)
1

1. `frame` changed

The view's position or size *relative to its superview* is altered. Its `center` property also changes if `frame.origin` is altered. `bounds.size` also changes if `frame.size` is altered. Subview positions remain relative to the parent's *new* internal bounds.

2

2. `bounds.origin` changed

The view's internal coordinate system *shifts*. Its content and subviews appear to move within its visible area. The view itself does not move within its superview. `frame` and `center` remain unchanged.

3

3. `bounds.size` changed

The view's internal size (and thus its external `frame.size`) is altered. This causes the view to resize, potentially triggering a layout pass for itself and its superview. `frame.origin` might also shift to maintain the view's `center` if `autoresizingMask` or constraints are set, but `bounds.origin` itself (if not (0,0)) remains its current value.

4

4. `transform` applied

The view's content is geometrically altered (rotated, scaled, translated). `bounds` generally describes the *untransformed* dimension for logical internal drawing, while `frame` becomes an axis-aligned bounding box around the transformed view, often growing to accommodate rotations.

Visualized execution hierarchy.

Powerful Guarantees

Consistent `center` with `transform`

A view's `center` property is always valid and consistent with its position in its superview's coordinate space, even when the `transform` property causes the `frame` to become undefined.

Drawing uses `bounds`

All `draw(dirtyRect:)` methods and custom `CALayer` drawing contexts always operate within the view's `bounds` coordinate system. The `(0,0)` point for drawing is always `bounds.origin`.

REAL PRODUCTION EXAMPLE: Implementing a Scrollable Content View

A common bug occurs when trying to implement a custom scroll view. Developers might try to move the `frame.origin` of a content view instead of changing the `bounds.origin` of the scroll container. This moves the container itself within its superview, rather than scrolling its internal content.

Impact / Results
Incorrect content scrolling behavior
View inadvertently moves off-screen
Breaks parent-child view hierarchy expectations
THE FIX or SOLUTION
swift
import UIKit

class CustomScrollView: UIView {
    let contentView: UIView

    override init(frame: CGRect) {
        self.contentView = UIView(frame: .zero)
        super.init(frame: frame)
        self.backgroundColor = .systemBackground
        self.clipsToBounds = true // Important: Clip content outside bounds
        setupContentView()
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    private func setupContentView() {
        addSubview(contentView)
        contentView.backgroundColor = .systemTeal

        // Simulate content much larger than the scroll view
        contentView.frame.size = CGSize(width: frame.width * 1.5, height: frame.height * 2.0)

        // Add some labels to content view for visual clarity
        let label1 = UILabel(frame: CGRect(x: 20, y: 20, width: 200, height: 30))
        label1.text = "Top Left Content"
        contentView.addSubview(label1)

        let label2 = UILabel(frame: CGRect(x: contentView.bounds.width - 220, y: contentView.bounds.height - 50, width: 200, height: 30))
        label2.text = "Bottom Right Content"
        contentView.addSubview(label2)
    }

    // Simulate scrolling by changing bounds.origin
    func scrollTo(xOffset: CGFloat, yOffset: CGFloat) {
        // Ensure the offset doesn't exceed content bounds
        let maxOffsetX = max(0, contentView.frame.width - bounds.width)
        let maxOffsetY = max(0, contentView.frame.height - bounds.height)

        let newX = min(maxOffsetX, max(0, xOffset))
        let newY = min(maxOffsetY, max(0, yOffset))

        // By changing bounds.origin, the internal coordinate system shifts,
        // making the content *appear* to scroll.
        // The CustomScrollView's frame itself does not move.
        self.bounds.origin = CGPoint(x: newX, y: newY)

        print("\n--- After scrolling to (\(Int(newX)), \(Int(newY))) ---")
        print("CustomScrollView Frame: \(frame)")
        print("CustomScrollView Bounds: \(bounds)")
        print("ContentView Frame (relative to scroll view): \(contentView.frame)") // Stays (0,0) with respect to initial bounds origin
    }
}

// Usage Example:
let container = UIView(frame: CGRect(x: 0, y: 0, width: 375, height: 600))
let myCustomScrollView = CustomScrollView(frame: CGRect(x: 20, y: 100, width: 300, height: 200))
container.addSubview(myCustomScrollView)

print("Initial ScrollView Frame: \(myCustomScrollView.frame)")     // {20, 100, 300, 200}
print("Initial ScrollView Bounds: \(myCustomScrollView.bounds)")    // {0, 0, 300, 200}

// Scroll some content
myCustomScrollView.scrollTo(xOffset: 100, yOffset: 150)

// You can also get a point relative to the superview's coordinate space:
let contentPointInSuperview = myCustomScrollView.convert(myCustomScrollView.contentView.bounds.origin, to: container)
print("Content View's (0,0) point in Container's coordinates: \(contentPointInSuperview)")
// This would be (20 - 100, 100 - 150) = (-80, -50) because bounds.origin is (100,150)

INTERVIEW PERSPECTIVE

Common Question

“Explain the difference between `frame` and `bounds` for a `UIView`. When would you use one over the other?”

Strong Answer

A strong answer highlights that `frame` is about a view's external position and size within its *superview's coordinate system*, while `bounds` is about a view's internal content rectangle within *its own local coordinate system*. `frame` is used for external positioning and sizing, common animations, and interacting with the superview. `bounds` is used for internal drawing, positioning subviews, and implementing scrolling. Mentioning how `transform` affects each, and the stability of `center` versus `frame` under transforms, demonstrates deeper understanding. A key point is that `bounds.origin` can be changed to 'scroll' content internally, without moving the view itself.

Interviewers Expect you to understand:
  • Clear distinction between coordinate systems
  • Correct use cases for each property (position vs. internal content/drawing)
  • Understanding of `transform`'s impact
  • Awareness of `center` property
KEY TAKEAWAY

`frame` defines a view's geometry relative to its superview; `bounds` defines its geometry relative to itself. Changing `bounds.origin` shifts internal content, not the view's position on screen. Master this difference for precise layout and custom drawing.

Common Interview Questions

What happens to `frame` when a view is rotated?

When a view is rotated using its `transform` property, its `frame` property becomes undefined or provides an axis-aligned bounding box. The `frame`'s `origin` and `size` will adjust to fully encompass the rotated view, even if it means the `frame`'s dimensions become larger than the view's original dimensions. The `bounds` and `center` properties remain more stable and reliable in such scenarios.

Can I have `frame.origin` and `bounds.origin` be different?

Yes, absolutely. By default, `bounds.origin` is `(0,0)`. `frame.origin` defines the view's position within its superview. If you change `bounds.origin` (e.g., to implement scrolling), `frame.origin` will remain unchanged, but the view's internal content will shift. The view itself does not move within its superview.

Which property should I use for converting points?

Use `convert(_:to:)`, `convert(_:from:)`, `convert(_:to:)`, and `convert(_:from:)` methods of `UIView`. These methods handle coordinate system transformations correctly. For example, to convert a point from a subview's coordinate system to its superview's coordinate system, you would use `subview.convert(point, to: superview)`.

Does `bounds.size` always equal `frame.size`?

Yes, `bounds.size` is generally equal to `frame.size` when the view's `transform` property is the identity transform (`.identity`). However, if a transformation like scaling is applied, `bounds.size` will reflect the active, logical size of the view, which will match `frame.size` only if the transform isn't also causing a rotation component that would create a larger axis-aligned bounding box. If rotation is involved, `frame.size` will be the size of the *axis-aligned bounding box*, which can be larger than `bounds.size`.

How do `frame` and `bounds` relate to Auto Layout?

Auto Layout ultimately computes the `frame` of each view. When you define constraints, the Auto Layout engine solves these constraints to determine the final position and size (`frame`) of every view. While you typically don't directly manipulate `frame` or `bounds` when using Auto Layout, understanding these properties helps you grasp how views are laid out under the hood and can be useful for debugging layout issues or performing custom drawing.

#UIKit#UIView#Geometry#Layout#iOS Development#Coordinate Systems