The Problem ð:
I encountered this situation where I needed to place an Image
next to my Text
.
However, the image had to be horizontally aligned with another image below, which was positioned in a different HStack
.
Check out these two scenarios:
The code responsible:
swift@State private var maxWidth: CGFloat? private var longText = "looooooongtextttttttt" private var shortText = "short" var body: some View { VStack(spacing: 20) { Group { HStack { labelView(text: longText) Spacer() Text("10") } } Divider() Group { HStack { labelView(text: shortText) Spacer() Text("100") } } } .padding() .background(.orange) .padding() }
But this is what I want to achieve: have the images aligned even if they are part of different HStack views.
Let us go through the solution step by step:
First, we need to define a custom PreferenceKey
that calculates the maximum width value based on the provided inputs:
swiftstruct MaxWidthPreferenceKey: PreferenceKey { static let defaultValue: CGFloat = 0 static func reduce( value: inout CGFloat, nextValue: () -> CGFloat ) { value = max(value, nextValue()) } }
After that, I like to create an extension on View
to avoid boilerplate code. This allows us to use trackMaxWidth()
on any view we want by simply applying the modifier. This approach utilizes GeometryReader
inside a background modifier so that we do not mess with the native SwiftUI alignment.
swiftextension View { func trackMaxWidth() -> some View { self.background(GeometryReader { geometry in Color.clear .preference(key: MaxWidthPreferenceKey.self, value: geometry.size.width) }) } }
Then we add a State
variable to dynamically adapt the width based on text length.
swift@State private var maxWidth: CGFloat?
And lastly we need a way to intercept those geometry reader changes:
swift.onPreferenceChange(MaxWidthPreferenceKey.self) { maxWidth = $0 }
I'll include the full code below for easier review:
swiftstruct ContentView: View { @State private var maxWidth: CGFloat? private var longText = "looooooongtextttttt" private var shortText = "short" var body: some View { VStack(spacing: 20) { Group { HStack { labelView(text: longText) Spacer() Text("10") } } Divider() Group { HStack { labelView(text: shortText) Spacer() Text("100") } } } .onPreferenceChange(MaxWidthPreferenceKey.self) { maxWidth = $0 } .padding() .background(.orange) .padding() } private func labelView(text: String) -> some View { HStack(spacing: 10) { Text(text) .font(.system(size: 16, weight: .regular)) .trackMaxWidth() .frame(width: maxWidth, alignment: .leading) .background(.green) Image(systemName: "heart") .font(.system(size: 13, weight: .light)) } .background(.yellow) } } struct MaxWidthPreferenceKey: PreferenceKey { static let defaultValue: CGFloat = 0 static func reduce( value: inout CGFloat, nextValue: () -> CGFloat ) { value = max(value, nextValue()) } } extension View { func trackMaxWidth() -> some View { self.background(GeometryReader { geometry in Color.clear .preference(key: MaxWidthPreferenceKey.self, value: geometry.size.width) }) } }