Skip to content

Instantly share code, notes, and snippets.

@nRewik
Last active April 20, 2024 04:26
Show Gist options
  • Star 6 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save nRewik/f510df64a72257cffd01294ee7107dfd to your computer and use it in GitHub Desktop.
Save nRewik/f510df64a72257cffd01294ee7107dfd to your computer and use it in GitHub Desktop.
SwiftUI - Make the children in VStack equal width, and expand dynamically

Problem

Begin with VStack that contain texts as children.

struct TestView: View {
    
    var body: some View {
        VStack(alignment: .trailing) {
            Text("Hello, World!")
                .background(Color.green)
            Text("Cat")
                .background(Color.red)
            Text("School")
                .background(Color.blue)
        }
        .padding()
    }
    
}

normal

I want the behaviour that the children expand dynamically depend on their contents, and they all have equal width.

The final result looks like this.

final

The solution

I use PreferenceKey and onPreferenceChange modifier. This allows children-parent communication.

  1. Firstly, we have maxWidth == nil. This means children have its own preferred width at the first render.
  2. MaxWidthEqualizer.notify will notify the preferred width back to the parent VStack via MaxWidthEqualizer(width:).
  3. MaxWidthEqualizer(width:) takes the max value from all the reported width. And assign to $maxWidth.
  4. Updating maxWidth state, will then trigger rendering with the new value of maxWidth.
struct TestView: View {
@State private var maxWidth: CGFloat?
var body: some View {
VStack(alignment: .trailing) {
Text("Hello, World!")
.modifier(MaxWidthEqualizer.notify)
.frame(width: maxWidth)
.background(Color.green)
Text("Cat")
.modifier(MaxWidthEqualizer.notify)
.frame(width: maxWidth)
.background(Color.red)
Text("School")
.modifier(MaxWidthEqualizer.notify)
.frame(width: maxWidth)
.background(Color.blue)
}
.padding()
.modifier(MaxWidthEqualizer(width: $maxWidth))
}
}
/// PreferenceKey to report the max width of the view.
struct MaxWidthPreferenceKey: PreferenceKey {
static var defaultValue: CGFloat = 0.0
// We `reduce` to just take the max value from all values reported.
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
value = max(value, nextValue())
}
}
/// Convenience view modifier that observe its size, and notify the value back to parent view via `MaxWidthPreferenceKey`.
///
struct MaxWidthNotify: ViewModifier {
/// We embed a transparent background view, to the current view to get the size via `GeometryReader`.
/// The `MaxWidthPreferenceKey` will be reported, when the frame of this view is updated.
private var sizeView: some View {
GeometryReader { geometry in
Color.clear.preference(key: MaxWidthPreferenceKey.self, value: geometry.frame(in: .global).size.width)
}
}
func body(content: Content) -> some View {
content.background(sizeView)
}
}
/// Convenience modifier to use in the parent view to observe `MaxWidthPreferenceKey` from children, and bind the value to `$width`.
///
struct MaxWidthEqualizer: ViewModifier {
@Binding var width: CGFloat?
func body(content: Content) -> some View {
content
.onPreferenceChange(MaxWidthPreferenceKey.self) { value in
let oldWidth: CGFloat = width ?? 0
if value > oldWidth {
width = value
}
}
}
}
extension MaxWidthEqualizer {
/// Syntactic sugar to help understand the code when adding MaxWidthNotify() to children.
static var notify: MaxWidthNotify {
MaxWidthNotify()
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment