5

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.
Sergiu Todirascu
  • 1,367
  • 15
  • 23
  • I don't really understand what you try to achieve. Even if we hug VStack (which is not hard), all internal texts are remained in center because they are not one text (to hug) they are 3 different views and will keep where they are with spacing 5 as you specified. If you want distribute text subviews, then how, because there are variants, and they depend on how many subviews can be in VStack. In short - there are ambiguities. – Asperi Jul 08 '20 at 18:17
  • 2
    I think he just wants to let the text / VStack determines the height of the whole HStack and then cut the images accordingly. – davidev Jul 08 '20 at 18:32

2 Answers2

2

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.

davidev
  • 7,694
  • 5
  • 21
  • 56
  • 1
    There's something wrong with using PreferenceKey's on views that are List rows, that makes `onPreferenceChange ` not to get triggered for every cell, and the image has 0 height. Key things I understood: – Sergiu Todirascu Jul 09 '20 at 18:40
  • 1
    1. `onPreferenceChange` gets called only when the height varies (I assume because preferences have a singleton storage). 2. For your example, even if it doesn't vary, it renders correctly, because it seems List reuses rows if they're the same. Try putting a model inside the view that changes across rows and you'll see what I mean. I'll update the question to accentuate that too. Sorry if I mislead, hid some info in the first place. Wasn't intentional. I'm still discovering all the dark corners of SwiftUI. – Sergiu Todirascu Jul 09 '20 at 18:40
0

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.

Azure
  • 229
  • 2
  • 5