0

I'm trying to make a simple VStack in SwiftUI where each view in the stack has the same height. The heights should be equal to the least amount of space required for the biggest view in the stack.

My minimum deployment target is iOS 13.

Here is an example of what I want: enter image description here

And this is what I currently have: enter image description here

In UIKit this is done easily by making the vertical UIStackView with .fillEqually distribution and then setting the contentCompressionResistancePriority to .required for the vertical component while keeping contentHuggingPriority to usually .defaultLow.

However in SwiftUI I'm not sure how to achieve this in a dynamic way. I don't want to set hardcoded frame heights.

I've tried setting .frame(maxHeight: .infinity) alongside .fixedSize(horizontal: false, vertical: true) but it doesn't seem to work in this scenario. I can do HStack with equal heights using this method and VStack with equal widths. But it doesn't seem to work for VStack with equal height and presumably also HStack equal width.

This is my code so far:

struct AccountSelectionView: View {
  @ObservedObject private(set) var viewModel: AccountSelectionViewModel
  
  var body: some View {
    VStack(spacing: 12) {
      ForEach(viewModel.model.accounts, id: \.title) { account in
        AccountView(account: account)
          .padding(16)
          .frame(maxHeight: .infinity)
          .overlay(RoundedRectangle(cornerSize: .init(width: 4, height: 4)).stroke(Color.outlineGrey, lineWidth: 1))
      }
    }
    .fixedSize(horizontal: false, vertical: true)
    
    Spacer()
  }
}
struct AccountView: View {
  let account: Account
  
  var body: some View {
    HStack {
      Image(uiImage: account.image)
        .padding(.trailing, 20)
      
      VStack(alignment: .leading) {
        Text(account.title)
          .font(.appFont("Heavy", size: 16))
        Text(account.message)
          .font(.appFont("Roman", size: 14))
      }
      Spacer()
    }
  }
}

I've also tried adding .frame(maxHeight: .infinity) and spacers to the AccountView but it didn't make a difference.

1 Answer 1

1

You can achieve this by deriving the maximum height of your view inside the list.

for that just replace your AccountSelectionView with the below code.

struct AccountSelectionView: View {
  @ObservedObject private(set) var viewModel: AccountSelectionViewModel
  
  var body: some View {
    VStack(spacing: 12) {
      ForEach(viewModel.model.accounts, id: \.title) { account in
        AccountView(account: account)
          .padding(16)
          .frame(minHeight: rowHeight)
          .overlay(RoundedRectangle(cornerSize: .init(width: 4, height: 4)).stroke(Color.outlineGrey, lineWidth: 1))
          .background(
              GeometryReader{ (proxy) in
                  Color.clear.preference(key: HeightPreferenceKey.self, value: proxy.size)
          })
          .onPreferenceChange(HeightPreferenceKey.self) { (preferences) in
              let currentSize: CGSize = preferences
              if (currentSize.height > self.rowHeight) {
                  self.rowHeight = currentSize.height
              }
          }
      }
    }
    Spacer()
  }
}

and here is your preferred key for saving maximum height.

struct HeightPreferenceKey: PreferenceKey {
    static func reduce(value: inout CGSize, nextValue: () -> CGSize) {
        nextValue()
    }
    
    typealias Value = CGSize
    static var defaultValue: Value = .zero
}
1
  • 1
    Missing @State var rowHeight: CGFloat = .zero in the AccountSelectionView. And .frame(maxHeight: .infinity) needs to be replaced with .frame(minHeight: rowHeight) However this works and seems to be the correct way of doing it. .fixedSize(horizontal: false, vertical: true) can be removed. Commented Mar 9, 2023 at 23:47

Not the answer you're looking for? Browse other questions tagged or ask your own question.