Create flexible interfaces in SwiftUI

Create flexible interfaces in SwiftUI

Learn how to bind your view’s size to its container in a SwiftUI app

When building interfaces, we want them to look good on different devices or screen sizes. Starting from iOS 17, views have been enriched with a new modifier, containerRelativeFrame(_:alignment:_:), which makes it easier to create user interfaces that automatically adjust based on the size of its nearest container.

This method places the view it is attached to into an invisible frame whose size adjusts based on the size of its nearest container.

To better understand how it works, imagine a box - your view - you want to place inside another box - your container. The containerRelativeFrame(_:alignment:_:) modifier allows adjusting the size of your view based on the size of the outer box where it is so that it will always maintain a consistent proportion between the two boxes’ sizes.

As stated in the documentation, this method can be used to set the size of the view’s width, height, or both accordingly to its nearest container whenever the former - the outer box - is one of the following:

  • The window presenting a view on iPadOS or macOS, or the screen of a device on iOS
  • A column of a NavigationSplitView
  • A NavigationStack
  • A tab of a TabView
  • A scrollable view like ScrollView or List
Margins or any safe area insets are calculated when the method is referring to container’s dimension.

This modifier comes with three different versions:

  1. containerRelativeFrame(_:alignment:)
func containerRelativeFrame(
    _ axes: Axis.Set,
    alignment: Alignment = .center
) -> some View

It's the simplest constructor provided for this method: it allows to specify only the axis - or the axes - which the view should adjust its size, and the alignment of the view within that frame.

Let's use the following view as an example:

struct CustomView: View {
    
    var body: some View {
        ScrollView (.horizontal){
            HStack{
                Circle()
                    .fill(Color.pink)
                    .frame(width: 100, height: 100)
                Circle()
                    .fill(Color.yellow)
                    .frame(width: 100, height: 100)
                
                Rectangle()
                    .fill(Color.green)
                    
                Circle()
                    .fill(Color.blue)
                    .frame(width: 100, height: 100)
                Circle()
                    .fill(Color.purple)
                    .frame(width: 100, height: 100)   
            }
        }
    }
    
}

The CustomView view has a horizontal ScrollView with an HStack - the container - with a series of shapes: all the circles have a fixed size while the rectangle - the view we will work with - doesn’t have a frame size at the moment.

0:00
/0:09
Rectangle()
    .fill(Color.green)
    .containerRelativeFrame(.horizontal)

By using containerRelativeFrame(.horizontal), the rectangle automatically adjusts its width to match that of the container.

0:00
/0:10
struct CustomView: View {
    
    var body: some View {
        ScrollView (.horizontal) {
            HStack {
                ...
            }
            // The container frame
            .frame(height: 200)
        }
    }
    
}

If we change the container’s frame size, it will adapt accordingly. Since it was defined for the frame to adapt on the horizontal axis, it will adjust the view’s size based on the width of the HStack container view.

0:00
/0:12

  1. containerRelativeFrame(_:alignment:_:)
func containerRelativeFrame(
    _ axes: Axis.Set,
    alignment: Alignment = .center,
    _ length: @escaping (CGFloat, Axis) -> CGFloat
) -> some View

This constructor allows for the specification of the axis, the alignment and a custom length calculation. You provide a closure that receives the container’s dimension and the axis - horizontal or vertical - and it returns the size for that axis.

Rectangle()
    .fill(Color.green)
    
    .containerRelativeFrame(.horizontal) { length, axis in
        if axis == .horizontal {
            return length * 2.0
        } else {
            return length + 5.0
        }
    }

In this example, the rectangle’s size is determined by a custom formula: if the axis is horizontal, the rectangle’s width will be twice the container’s width. This version provides more control over how the view scales relative to its container.

0:00
/0:22

  1. containerRelativeFrame(_:count:span:spacing:alignment:)
func containerRelativeFrame(
    _ axes: Axis.Set,
    count: Int,
    span: Int = 1,
    spacing: CGFloat,
    alignment: Alignment = .center
) -> some View

This constructor is designed for a more complex layout. It divides the container’s length along a specified axis into a number of equal segments - columns for the horizontal axis and rows for the vertical one and allows the specification of:

    1. count: the number of rows or columns that the length of the container should be divided into along the defined axis;
    2. span: how many rows or columns the view should occupy;
    3. spacing: the space in between the segments.
Rectangle()
    .fill(Color.green)
    .containerRelativeFrame(.horizontal, count: 15, span: 1, spacing: 15.0)

The container - the HStack- is imagined as being divided into 15 equal columns (with spacing in between). The rectangle is set to span one column. This approach is practical when aligning elements to a grid or creating layouts requiring evenly dividing space.

0:00
/0:09

Using containerRelativeFrame(_:alignment:_:) allows the creation of adaptive layouts that look good on any device. This method fosters a way to design user interfaces that are both flexible and responsive with minimal effort, and each version of its constructor offers a different level of control, allowing the layout to be tailored accordingly to the design needs.