skip to Main 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!

2

Answers


  1. 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

    Login or Signup to reply.
  2. 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 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. 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 the height will be the height of the tallest card. You can then use maxHeight: .infinity to have all the cards expand to this height, perhaps using alignment: .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)
                }
                .frame(maxWidth: .infinity, maxHeight: maxHeight, alignment: .top)
                .padding(16)
                .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 re-usable then you could always make maxWidth a parameter of type CGFloat? too, in the same way as maxHeight has been parameterised above. You could also support a default of nil, so that it behaves in the same way as you had it before.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search