iosswiftswiftuihstack

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!


Solution

  • For the width, you were doing it the right way by using maxWidth: .infinity, except that this needs to be applied before the background and other decorations (like shadow and clip shape). So:

    The first way involves less changes (but see the note about reusability after the screenshot below).

    For the height, I would suggest one of the following two techniques:

    Text(title)
        .font(.subheadline)
        .lineLimit(2, reservesSpace: true)
    
    struct TestViewHStack: View {
    
        let strings = ["Short", "some long text", "short"]
    
        private func cardRow(fullHeight: Bool = false) -> some View {
            HStack {
                ForEach(strings, id: \.self) { title in
                    CardView(title: title, maxHeight: fullHeight ? .infinity : nil)
                }
            }
        }
    
        var body: some View  {
            cardRow()
                .hidden()
                .overlay{
                    cardRow(fullHeight: true)
                }
        }
    }
    
    struct CardView: View {
        let title: String
        let maxHeight: CGFloat?
    
        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, maxHeight: maxHeight, alignment: .top)
                .background(Color.white)
                .clipShape(RoundedRectangle(cornerRadius: 16))
                .shadow(color: .gray.opacity(0.2), radius: 8, x: 0, y: 2)
        }
    }
    

    Screenshot

    If you are concerned that moving maxWidth: .infinity into CardView stops it from being reusable, then you could always make maxWidth a parameter of type CGFloat? in just the same way as maxHeight has been parameterized above. You could also support a default of nil, so that it behaves in the same way as you had it before:

    struct CardView: View {
        let title: String
        var maxWidth: CGFloat?
        var maxHeight: CGFloat?
    
        var body: some View {
                VStack(spacing: 8) {
                    // ...
                }
                .padding(16)
                .frame(maxWidth: maxWidth, maxHeight: maxHeight, alignment: .top)
                // decoration as before
        }
    }
    

    This version of CardView behaves exactly the same as you originally had it. When used in the context of TestViewHStack, the function cardRow shown in this answer would need to specify the maxWidth explicitly:

    ForEach(strings, id: \.self) { title in
        CardView(
            title: title,
            maxWidth: .infinity,
            maxHeight: fullHeight ? .infinity : nil
        )
    }