Horizontally Align Images with Dynamic Text Views in a VStack in SwiftUI 🚧

No imageSilviu Vranau
29 July, 2024

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:


No Image
The heart images are not horizontally aligned!

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.

No Image
Aligned no matter the length!

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:

swift
struct 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.

swift
extension 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:

swift
struct 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) }) } }

Thanks for stopping by! ðŸŠī