Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

SwiftUI HStack inner Views have fill entire width equally and have same height, regardless of the content

So I just need the views within an HSTack to each take the entire width available to them so the space is evenly distributed and all have the same width. As for height, they should also all have the same height regardless of text length.

Current code:

struct TestViewHStack: View {
    
    let strings = ["Short", "some long text", "short"]
    
    var body: some View  {
            HStack {
                ForEach(strings, id: \.self) { title in
                    CardView(title: title)
                        .frame(maxWidth: .infinity)
                }
            }
        }
    }

struct CardView: View {
    let title: String
    
    var body: some View {
            VStack(spacing: 8) {
                Image(systemName: "star.fill")
                    .resizable()
                    .frame(width: 20, height: 20)
                Text(title)
                    .font(.subheadline)
            }
            .padding(16)
            .background(Color.white)
            .clipShape(RoundedRectangle(cornerRadius: 16))
            .shadow(color: .gray.opacity(0.2), radius: 8, x: 0, y: 2)
    }
}

Output:

image

I thought setting .frame(maxWidth: .infinity) would make it each take the entire width available, but it doesnt seem to do that. Also tried messing with frames and fixedSize() modifiers but wasnt able to achieve the desired result.

Thanks!

like image 474
stompy Avatar asked Sep 19 '25 11:09

stompy


1 Answers

For the width:

They already have the same width, but you cant see it because you apply the height after setting the background. So reorder the modifiers to make it visible:

struct TestViewHStack: View {

    let strings = ["Short", "some long text", "short"]

    var body: some View  {
        HStack {
            ForEach(strings, id: \.self) { title in
                CardView(title: title)
                    // .frame(maxWidth: .infinity) // 👈 This is after the background framing
            }
        }
    }
}

struct CardView: View {
    let title: String

    var body: some View {
        VStack(spacing: 8) {
            Image(systemName: "star.fill")
                .resizable()
                .frame(width: 20, height: 20)
            Text(title)
                .font(.subheadline)
        }
        .padding(16)
        .frame(maxWidth: .infinity) // 👈 This does the job as expected
        .background(Color.white)
        .clipShape(RoundedRectangle(cornerRadius: 16))
        .shadow(color: .gray.opacity(0.2), radius: 8, x: 0, y: 2)
    }
}
Visible Equal Widths Demo:

Demo 1

For the height:

If you just need a fixed height, just add a maxHeight to the frame and control the height of the stack.

But it seems like you don't know the height and you need all items to have the same size with the tallest one. So you need to let them size themselves and update anyone with the shorter height. This is where onPreferenceChange comes in and requires some work.

struct TestViewHStack: View {

    let strings = ["Short", "some long text", "short"]
    @State private var maxCardHeight: CGFloat = 10

    var body: some View  {
        HStack {
            ForEach(strings, id: \.self) { title in
                CardView(title: title)
                    .fixedSize(horizontal: false, vertical: true)
                    .background {
                        GeometryReader { proxy in
                            Color.clear.preference(
                                key: MaxValue.self,
                                value: proxy.size.height
                            )
                        }
                    }
                    .onPreferenceChange(MaxValue.self) {
                        maxCardHeight = $0
                    }
                    .frame(maxWidth: .infinity, maxHeight: maxCardHeight)
                    .background(Color.white)
                    .clipShape(RoundedRectangle(cornerRadius: 16))
                    .shadow(color: .gray.opacity(0.2), radius: 8, x: 0, y: 2)
            }
        }
    }
}

public enum MaxValue: PreferenceKey {
    public static let defaultValue = CGFloat()
    public static func reduce(value: inout Value, nextValue: () -> Value) { 
        print("value: \(value), next: \(nextValue())")
        value = max(value, nextValue())
    }
}

struct CardView: View {
    let title: String

    var body: some View {
        VStack(spacing: 8) {
            Image(systemName: "star.fill")
                .resizable()
                .frame(width: 20, height: 20)
            Text(title)
                .font(.subheadline)
        }
        .padding(16)
    }
}

Demo 2 Note that you must first size and then apply the background. So You need to rearrange your UI logic

like image 77
Mojtaba Hosseini Avatar answered Sep 21 '25 03:09

Mojtaba Hosseini