Content hugging priority behaviour in SwiftUI

Viewed 2993

I have a List made of cells, each containing an image, and a column of text, which I wish laid out in a specific way. Image on the left, taking up a quarter of the width. The rest of the space given to the text, which is left-aligned.

Here's the code I got:

struct TestCell: View {
    let model: ModelStruct

    var body: some View {
        HStack {
            Image("flag")
                .resizable()
                .aspectRatio(contentMode: .fit)
                .frame(width: UIScreen.main.bounds.size.width * 0.25)
            VStack(alignment: .leading, spacing: 5) {
                Text("Country: Moldova")
                Text("Capital: Chișinău")
                Text("Currency: Leu")
            }
            .frame(minWidth: 0, maxWidth: .infinity, alignment: .leading)
        }
    }
}

struct TestCell_Previews: PreviewProvider {
    static var previews: some View {
        TestCell()
            .previewLayout(.sizeThatFits)
            .previewDevice("iPhone 11")
    }
}

And here are 2 examples:

As you can see, the height of the whole cell varies based on the aspect ratio of the image.

$1M question - How can we make the cell height hug the text (like in the second image) and not vary, but rather shrink the image in a scaleAspectFit manner inside the allocated rectangle

Note!

  1. The text's height can vary, so no hardcoding.
  2. Couldn't make it work with PreferenceKeys, as the cells will be part of a List, and there's some peculiar behaviour I'm trying to grasp around cell reusage, and onPreferenceChange not being called when 2 consecutive cells have the same height. To exhibit all this combined behaviour, make sure your model varies between cells when you test it.
2 Answers

Here is a possible solution, however it uses GeometryReader inside the background property of the VStack, to detect their height. That height is being applied to the Image then. I used SizePreferenceKey from this solution.

struct SizePreferenceKey: PreferenceKey {
    typealias Value = CGSize
    static var defaultValue: Value = .zero

    static func reduce(value _: inout Value, nextValue: () -> Value) {
        _ = nextValue()
    }
}

struct ContentView6: View {

    @State var childSize: CGSize = .zero
    
    var body: some View {
        HStack {
            Image("image1")
                .resizable()
                .aspectRatio(contentMode: .fit)
                .frame(width: UIScreen.main.bounds.size.width * 0.25, height: self.childSize.height)
            VStack(alignment: .leading, spacing: 5) {
                Text("Country: Moldova")
                Text("Capital: Chișinău")
                Text("Currency: Leu")
            }
            .frame(minWidth: 0, maxWidth: .infinity, alignment: .leading)
            .background(
                  GeometryReader { proxy in
                    Color.clear.preference(key: SizePreferenceKey.self, value: proxy.size)
                   }
                 )
        }
        .onPreferenceChange(SizePreferenceKey.self) { preferences in
          self.childSize = preferences
        }
        .border(Color.yellow)

    }
      

}

Will look like this.. you can apply different aspect ratios for the Image of course.

This is what worked for me to constrain a color view to the height of text content in a cell:

A height reader view:

struct HeightReader: View {
    @Binding var height: CGFloat
    var body: some View {
        GeometryReader { proxy -> Color in
            update(with: proxy.size.height)
            return Color.clear
        }
    }
    private func update(with value: CGFloat) {
        guard value != height else { return }
        DispatchQueue.main.async {
            height = value
        }
    }
}

You can then use the reader in a compound view as a background on the view you wish to constrain to, using a state object to update the frame of the view you wish to constrain:

struct CompoundView: View {
    @State var height: CGFloat = 0
    var body: some View {
        HStack(alignment: .top) {
            Color.red
                .frame(width: 2, height: height)
            VStack(alignment: .leading) {
                Text("Some text")
                Text("Some more text")
            }
            .background(HeightReader(height: $height))
        }
    }
}

struct CompoundView_Previews: PreviewProvider {
    static var previews: some View {
        CompoundView()
    }
}

enter image description here

I have found that using DispatchQueue to update the binding is important.

Related