The containerRelativeFrame Modifier In SwiftUI

Posted in SwiftUI

February 21st, 2025

⏱ Reading Time: 7 mins

In iOS 17, SwiftUI introduced the containerRelativeFrame view modifier, a powerful and handy tool that allows us to set the size of a view in relation to its parent view. Through a few initializers, this modifier enables us to create adaptive user interfaces which otherwise would require the employment of more complicated approaches, such as using the GeometryReader container.

In this post we'll explore the various initializers of the containerRelativeFrame modifier and meet the results of the different arguments we'll provide them with. It's definitely a modifier you can experiment a lot with, but if you haven't used it before, I'm certain you'll be pleasantly surprised by how easy it makes it to specify adaptable view sizes, always related to their container views.


Getting started with the containerRelativeFrame modifier

First off, it's important to highlight a significant detail about the containerRelativeFrame modifier, as it doesn't work with all available container views in SwiftUI. This modifier has effect when the view that's applied on is a child view in any of the following; the main view (app window's view), tab views, navigation stacks and scroll views. The official documentation page from Apple enumerates all valid containers. It doesn't have any effect when the target view is enclosed in other views, such as VStack, HStack, ForEach, etc. Instead, the modifier searches up in the hierarchy until an expected container view is found, and uses its frame in order to accommodate the view's size. You'll find that out next, as two containers are skipped (ForEach and LazyVStack) until a ScrollView is found.

With that in mind, let's start exploring the containerRelativeFrame using an easy example. In the following view we have a collection of colors which we are presenting vertically in a scroll view:

At the time being no size management of the color views is taking place. The above implementation results to the following outcome:

Taking the simplest scenario, let's say that we want each color to occupy the entire available space vertically. Instead of setting frames manually, using GeometryReader, or resorting to any other possible solution, we'll put containerRelativeFrame in motion and we'll ask it to resize each color view vertically so it expands to the parent view's available height. We do that by passing as argument the axis we want the view to expand on:

We could specify both axes if we'd like as arguments, even though the horizontal axis has no effect other than the default in this case --later we'll see how we can affect size on horizontal axis too:

Now, each color occupies the entire available area horizontally in the scroll view. In general, note that the available area of the container view is what it remains after subtracting any safe area insets applied to it.

Controlling the size of views in the container

The initializer demonstrated previously is useful for simple cases where no size calculation in any dimension is necessary. However, we gain a lot more control with the containerRelativeFrame(_:length:) initializer. Here, length is a closure where we set a desired size value for the view on each axis. That length can be calculated, be a proportion of the parent view's length on the horizontal or vertical axis, or be a specific hardcoded value.

Additionally, we saw previously that containerRelativeFrame had no effect on the horizontal axis. Now, we're going to change that, and enforce a size change on that axis too.

To continue, let's apply the containerRelativeFrame modifier using this, more powerful, initializer:

Notice here that we ask for size changes in both horizontal and vertical dimensions. Inside the closure, the first parameter named length contains the length of the container view for each axis. The latter is specified in the axis parameter.

Inside the closure we implement any logic that's useful for the best appearance of our views. In this example, we'll make the length of each color view half of the scroll view's width on horizontal axis, and one third of the scroll view's height vertically:

See how we make use of length in the above code. Of course, we can write it in a shorter way using the ternary operator:

With just that few lines of undoubtedly simple code, we manage to resize color views as we'd like to:

Needless to say, but containerRelativeFrame effects work on both orientations:

Obviously, you can always disregard the length value and set hardcoded sizes:

Splitting container view size in rows and columns

Next, let's meet the containerRelativeFrame(_:count:span:spacing:) initializer, which becomes handy when we prefer to divide the available container view's area into rows or columns. In particular:

  • count is the number of rows or columns we'd like to divide the available area in.
  • span is the number of rows or columns we'd like each view to occupy.

To understand what spacing does, you'd better take a look at the following calculations taken from the Apple docs:

It turns out that spacing affects the calculation of the available area of the container view, and the size of each child view. Definitely, experimenting on your own will help better understand how it works.

Back to our example, let's say that we'd like to split the available area in 10 rows, and each view should occupy 2 rows. We'll keep spacing to 0:

Indeed, each subview takes two rows and the total occupied height is calculated accordingly automatically. Now, and for the sake of the demonstration, let's change the spacing value to 50:

You'll notice that the available area and the size of each view have been decreased, as the result of the change on spacing:

We can always include the horizontal axis in the first argument, but how much sense that makes depends on the use case:

An important detail

Before we get going with the next initializer of the containerRelativeFrame modifier, it would be useful to highlight at this point a detail, which although small, it might not be obvious right away, yet can have a real impact.

That said, keep in mind that you can apply multiple containerRelativeFrame modifiers in a child view. Doing so allows to apply different logic on each axis and achieve the layout you're after. For instance, the following splits the available vertical area into rows, while it makes the width of each child view equal to the 75% of the scroll view's width:

Generally, feel free to make any combination that would lead to the desired outcome.

Changing the default alignment of the views

The above are not the only available initializers of the containerRelativeFrame modifier. There are two more that allow to position a child view to a location other than the default one:

  • containerRelativeFrame(_:alignment:)
  • containerRelativeFrame(_:alignment:length:)

The alignment parameter takes an Alignment value, which defines the view's position within its container. To see how it works, let's consider another example, where we'll simply show a color view in the main view of the app's window. Along with the color view, we'll set the size of the color view using an initializer of the containerRelativeFrame that we've already met:

We can now apply the containerRelativeFrame once again, specifying the topLeading edge as the new alignment:

The topLeading value changes the position both in the horizontal and vertical axes. That's possible because we define them both above: [.horizontal, .vertical]. However, if one axis is omitted, the alignment will only affect the remaining specified axis.

For instance, let's remove the .vertical axis:

The result is the following:

Notice that the view didn't move to the vertical axis at all, however it moved indeed to the leading edge of the view.

Conclusion

The containerRelativeFrame is a powerful view modifier that can replace in many cases other, more complicated methods to layout child views in containers, including the GeometryReader container, view modifiers to set frame, position, offset, and more. However, always remember that it works with specific container views, and its availability starts as of iOS 17. To use it, it all comes down to choosing the appropriate initializer, while occasionally, best results can be achieved by applying more than one occurrences of the containerRelativeFrame modifier. And remember, not all initializers have the same, nor any effects, in all supported container views. Experiment with containerRelativeFrame, and you'll find yourself achieving great user interfaces with less code than before. Thanks for reading!


Stay Up To Date

Subscribe to my newsletter and get notifiied instantly when I post something new on SerialCoder.dev.

    We respect your privacy. Unsubscribe at any time.