Search code examples
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:

    • you can either move the .frame modifier from TestViewHStack to CardView
    • or, you could move all the decoration from CardView to TestViewHStack.

    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)
    
    • If you just want to use the minimum space necessary, then the space needed can be established using a hidden version of the same content. The hidden version forms the footprint and its height will be the height of the tallest card. The visible content is then shown as an overlay over the footprint.

      An overlay is constrained by the size of the view it is applied to, so maxHeight: .infinity can be used to have all the cards expand to the full height of the overlay, this being the height of the tallest card. Use alignment: .top if you want the stars to be aligned at the top:

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