Proportional layout with SwiftUI
The power of custom layouts
Starting from iOS 16, SwiftUI gives us the option to build custom layouts that can control very precisely where each subview should be placed. We have the option to build layouts that can be used just as the VStack, HStack or any other built in Layout you may be familiar with from SwiftUI.
This new tool creates an opportunity for us to really easily create layouts that size their subviews proportionally to its own dimensions, similar to flex box in CSS
Before custom Layout
First, let’s see how would we implement something like this without using any custom layouts. The key for achieving the desired results is the GeometryReader
Using it, SwiftUI exposes the available space to our view hierarchy and makes it possible for us to do calculations based on it. Let’s see the code
Group {
GeometryReader { proxy in
HStack(spacing: 0) {
Color.red.frame(width: proxy.size.width * 0.5)
Color.green.frame(width: proxy.size.width * 0.2)
Color.blue.frame(width: proxy.size.width * 0.3)
}
}
}
.padding()
.frame(width: 300, height: 300)
As you can see, this solution works, but there are a couple of issues
- As you may already know, Geometry reader tries to take up as much space as possible thus making it more complicated to achieve UIs that adapt well to different screen sizes and content sizes
- This solution is rather verbose and it would be pretty challenging to encapsulate this in an easily reusable component
Container values
Before we look at the custom layout we are making, let’s talk about container values real quick. Container values were introduced in iOS 18 and are meant to provide a way for passing values from the different views back to the containers, AKA our layout. To define a container value, you can use the @Entry property wrapper.
extension ContainerValues {
@Entry var proportion: Double = 1
}
public extension View {
func proportion(_ value: Double) -> some View {
self.containerValue(\.proportion, value)
}
}
Let’s build the container
Finally let’s build our proportional stack. Start by creating a new layout
public struct ProportionalStack: Layout {
//...
}
The layout protocol requires two methods to be implemented. First add sizeThatFits
. This method is called to determine the optimal size that works both for your layout and its surrounding. In this method you could provide any special logic, but for now we just accept the proposed size.
public func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
return proposal.replacingUnspecifiedDimensions()
}
Notice the replacingUnspecifiedDimensions
which is used to replace any nil values in the proposal by a non nil value
The second method to implement is placeplaceSubviews
. This is where we can perform the actual logic for placing the subviews and sizing them according to the given proportion
public func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) {
//...
}
We will use 1 as the default value for cases when no proportion is provided, to calculate the final factor for each view, we will need to calculate the sum of all proportion values then divide each views proportion with the sum
let totalSizeFactor = subviews.map(\.containerValues.proportion).reduce(0, +)
var origin = bounds.origin
for view in subviews {
let proportion = view.containerValues.proportion / totalSizeFactor
// ...
}
Finally we can place our view to the screen. To keep track of where each view ends, we are going to use the origin variable
let size = CGSzie(width: bounds.size.width * proportion, height: bounds.size.height)
view.place(at: origin, proposal: .init(size))
origin.x += size.width
That’s it! Our layout is ready to be used
ProportionalStack(direction: .horizontal) {
Color.red.proportion(3)
Color.green
Color.blue.proportion(2)
}
.frame(width: 300, height: 300)
Next steps
This layout can be extended to work in both direction (horizontal and vertical) or to fill the least amount of space the subviews need instead of just accepting the proposed size.
To see the full code or to start using the layout as a swift package, check out my repository
Related articles
Here are some more articles that may interest you. Check them out!
The RawRepresentable protocol
published on February 15, 2025
SwiftIf you’ve ever used a Swift enum with a raw value, you may not even realise that you have already worked with the RawRepresentable protocol. But what is it and how can you make use of it? In this article we are going to investigate the RawRepresentable protocol and I’m going to show you some neat ways for using the protocol to our advantage.
Read moreThe @ and # symbols in Swift
published on January 2, 2025
SwiftWhen a new feature is introduced in a programming language, we as developers are often quick to assume it will solve many of their challenges. The excitement to experiment with such features is natural, but it’s often accompanied by the realisation that every solution brings its own set of unique challenges. This was no different when actors were introduced.
Read moreThe reentrancy problem
published on December 16, 2024
SwiftConcurrencyWhen a new feature is introduced in a programming language, we as developers are often quick to assume it will solve many of their challenges. The excitement to experiment with such features is natural, but it’s often accompanied by the realisation that every solution brings its own set of unique challenges. This was no different when actors were introduced.
Read more