SwiftUI Modifiers Deep Dive: containerRelativeFrame
Published on Sep 4, 2024Get a quick glimpse of how Tiny Currency simplifies currency conversion with up to date rates, multi-currency support, and easy-to-use widgets. Perfect for on-the-go use, our app ensures you're always prepared, no matter where your travels take you.
📱 iOS 17.0+
On this deep dive, we are exploring the containerRelativeFrame
SwiftUI modifier, which was added in iOS 17.
Apple's Docs:
Positions this view within an invisible frame with a size relative to the nearest container.
Apple's Discussion:
Use this modifier to specify a size for a view’s width, height, or both that is dependent on the size of the nearest container. Different things can represent a “container” including:
• 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
The size provided to this modifier is the size of a container like the ones listed above subtracting any safe area insets that might be applied to that container.
Usage
So what does usage for this modifier actually look like? Apple's documentation shows us a few examples. The following example will have each purple rectangle occupy the full size of the screen on iOS:
ScrollView(.horizontal) {
LazyHStack(spacing: 0.0) {
ForEach(items) { item in
Rectangle()
.fill(.purple)
.containerRelativeFrame([.horizontal, .vertical])
}
}
}
What exactly is happening here? Well, pretty simply, the Rectangle fills the "container" but not the ScrollView itself. So for this example, each item will lay out a purple rectangle that fills the entire screen. So if items
in this case had 3 items, it would cover 3 screen widths worth of space.
Let's take a look at another of the documentations examples:
ScrollView(.horizontal) {
LazyHStack(spacing: 10.0) {
ForEach(0..<3) { item in
Rectangle()
.fill(.purple)
.aspectRatio(3.0 / 2.0, contentMode: .fit)
.containerRelativeFrame(.horizontal, count: 4, span: 3, spacing: 10.0)
}
}
}
.safeAreaPadding(.horizontal, 20.0)
This version is interesting, it adds count and span. Apple describes it like so:
Use the containerRelativeFrame(_:count:span:spacing:alignment:) modifier to size a view such that multiple views will be visible in the container. When using this modifier, the count refers to the total number of rows or columns that the length of the container size in a particular axis should be divided into. The span refers to the number of rows or columns that the modified view should actually occupy. Thus the size of the element can be described like so:
let availableWidth = (containerWidth - (spacing * (count - 1)))
let columnWidth = (availableWidth / count)
let itemWidth = (columnWidth * span) + ((span - 1) * spacing)
So what exactly does this mean?
Basically, the count and span are used to calculate the width of each column (or the height of each row) within the container. Think of count as how many parts the ScrollView should be split into, and think of the span as how many parts to allocate to each view.
So for the above example, we set a count of 4 and a span of 3, we can simplify this by saying that each item takes up about 3/4 of the width of the container.
Use Cases
A lot of apps these days, including Apple's own apps such as Apple Music or Podcasts are showing vertical ScrollViews of horizontal ScrollViews. These horizontally scrolling views can signify collections.
One of my favorite use cases is the simplest one, if you want a simple Text to take up the entire container, you can do so easily as well.
// Collections on the left
ScrollView {
ForEach(0..<5) { index in
VStack(alignment: .leading) {
Text("Collection \(index)")
.font(.title2)
.bold()
ScrollView(.horizontal) {
LazyHStack(spacing: 10.0) {
ForEach(0..<3) { item in
RoundedRectangle(cornerRadius: 12)
.fill(.purple)
.aspectRatio(3.0 / 2.0, contentMode: .fit)
.containerRelativeFrame(.horizontal, count: 4, span: 3, spacing: 10.0)
}
}
}
.scrollIndicators(.hidden)
}
.safeAreaPadding(.horizontal, 20.0)
}
}
// Text on the right
Text("Hello World")
.containerRelativeFrame([.vertical, .horizontal])
.background(.blue)