Simplest Way to Set Specific Width Ratios for Child Elements in an HStack
in SwiftUI
Hello,
I am working with SwiftUI and wondering if there's a straightforward method to set the widths of child elements within an HStack
to specific ratios, regardless of the number of characters. For example, I would like the widths of Text1
, Text2
, and Text3
to have a ratio of 1:2:1 respectively.
Here is the current implementation:
HStack {
Text("Text1")
Text("Text2")
Text("Text3")
}
I understand that using GeometryReader
can solve this issue, but I'm looking for a more elegant solution—something similar to applying weights to the frames, such as frame(maxWidth: .infinity, weight: 2)
—to smartly distribute the widths among the child elements.
Has anyone found an effective and simpler approach to achieve this?
It’s fine even if using Grid!
Thank you for your assistance!
A custom Layout
can be used for this.
To pass a parameter to a custom layout, you need to define a LayoutValueKey
. Let's do this for the layout weight. As a convenience, a view extension can also be defined, for applying the layout weight.
private struct LayoutWeight: LayoutValueKey {
static let defaultValue = 1
}
extension View {
func layoutWeight(_ weight: Int) -> some View {
layoutValue(key: LayoutWeight.self, value: weight)
}
}
Here is an example Layout
implementation which works as follows:
A cache is used to save the layout weights of the subviews. The maximum ideal height is also cached.
The function sizeThatFits
computes the widths for the subviews by dividing the container width in accordance with the weights of the subviews. The size returned by this function is always the full container width, but the height is the maximum height required by the subviews.
The function placeSubviews
works similarly. Each subview is positioned at the center of the space available to it. There is no spacing between subviews.
struct WeightedHStack: Layout {
typealias Cache = ViewInfo
struct ViewInfo {
let weights: [Int]
let idealMaxHeight: CGFloat
var isEmpty: Bool {
weights.isEmpty
}
var nWeights: Int {
weights.count
}
var sumOfWeights: Int {
weights.reduce(0) { $0 + $1 }
}
}
func makeCache(subviews: Subviews) -> ViewInfo {
var weights = [Int]()
var idealMaxHeight = CGFloat.zero
for subview in subviews {
let idealViewSize = subview.sizeThatFits(.unspecified)
idealMaxHeight = max(idealMaxHeight, idealViewSize.height)
weights.append(subview[LayoutWeight.self])
}
return ViewInfo(weights: weights, idealMaxHeight: idealMaxHeight)
}
func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ViewInfo) -> CGSize {
var maxHeight = cache.idealMaxHeight
if !cache.isEmpty, subviews.count == cache.nWeights, let containerWidth = proposal.width {
let unitWidth = containerWidth / CGFloat(cache.sumOfWeights)
for (index, subview) in subviews.enumerated() {
let viewWeight = cache.weights[index]
let w = CGFloat(viewWeight) * unitWidth
let viewSize = subview.sizeThatFits(ProposedViewSize(width: w, height: nil))
maxHeight = max(maxHeight, viewSize.height)
}
}
return CGSize(width: proposal.width ?? 10, height: maxHeight)
}
func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ViewInfo) {
if !cache.isEmpty, subviews.count == cache.nWeights {
let unitWidth = bounds.width / CGFloat(cache.sumOfWeights)
var minX = bounds.minX
for (index, subview) in subviews.enumerated() {
let viewWeight = cache.weights[index]
let w = CGFloat(viewWeight) * unitWidth
let viewSize = subview.sizeThatFits(ProposedViewSize(width: w, height: bounds.height))
let h = viewSize.height
let x = minX + ((w - viewSize.width) / 2)
let y = bounds.minY + ((bounds.height - h) / 2)
subview.place(at: CGPoint(x: x, y: y), proposal: ProposedViewSize(width: w, height: h))
minX += w
}
}
}
}
Some examples of use:
1. Three simple Text
views
WeightedHStack {
Text("Text1").layoutWeight(1).background(.yellow)
Text("Text2").layoutWeight(3).background(.orange)
Text("Text3").layoutWeight(1).background(.yellow)
}
.border(.red)
2. Three text views, each with padding and maximum width
WeightedHStack {
Text("Text1")
.padding()
.frame(maxWidth: .infinity)
.layoutWeight(1)
.background(.yellow)
Text("Text2")
.padding()
.frame(maxWidth: .infinity)
.layoutWeight(3)
.background(.orange)
Text("Text3")
.padding()
.frame(maxWidth: .infinity)
.layoutWeight(1)
.background(.yellow)
}
.border(.red)
3. As above, but using a larger text in the middle
let loremIpsum = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."