6

I am trying to create a view in SwiftUI where the background of the image on the left should scale vertically based on the height of the text on the right.

I tried a lot of different approaches, from GeometryReader to .layoutPriority(), but I haven't managed to get any of them to work.

Current state:

Current state

Desired state:

enter image description here

I know that I could imitate the functionality by hardcoding the .frame(100) for the example I posted, but as text on the right is dynamic, that wouldn't work.

This is full code for the view in the screenshot:

import SwiftUI

struct DynamicallyScalingView: View {
    var body: some View {
        HStack(spacing: 20) {
            Image(systemName: "snow")
                .font(.system(size: 32))
                .padding(20)
                .background(Color.red.opacity(0.4))
                .cornerRadius(8)

            VStack(alignment: .leading, spacing: 8) {
                Text("My Title")
                    .foregroundColor(.white)
                    .font(.system(size: 13))
                    .padding(5)
                    .background(Color.black)
                    .cornerRadius(8)
                Text("Dynamic text that can be of different leghts. Spanning from one to multiple lines. When it's multiple lines, the background on the left should scale vertically")
                    .font(.system(size: 13))
            }
        }
        .padding(.horizontal)
    }
}

struct DailyFactView_Previews: PreviewProvider {
    static var previews: some View {
        DynamicallyScalingView()
    }
}
Nace
  • 2,854
  • 1
  • 15
  • 21

2 Answers2

14

Here is a solution based on view preference key. Tested with Xcode 11.4 / iOS 13.4

demo

struct DynamicallyScalingView: View {
    @State private var labelHeight = CGFloat.zero     // << here !!

    var body: some View {
        HStack(spacing: 20) {
            Image(systemName: "snow")
                .font(.system(size: 32))
                .padding(20)
                .frame(minHeight: labelHeight)       // << here !!
                .background(Color.red.opacity(0.4))
                .cornerRadius(8)

            VStack(alignment: .leading, spacing: 8) {
                Text("My Title")
                    .foregroundColor(.white)
                    .font(.system(size: 13))
                    .padding(5)
                    .background(Color.black)
                    .cornerRadius(8)
                Text("Dynamic text that can be of different leghts. Spanning from one to multiple lines. When it's multiple lines, the background on the left should scale vertically")
                    .font(.system(size: 13))
            }
            .background(GeometryReader {      // << set right side height
                Color.clear.preference(key: ViewHeightKey.self, 
                    value: $0.frame(in: .local).size.height) 
            })
        }
        .onPreferenceChange(ViewHeightKey.self) { // << read right side height
            self.labelHeight = $0        // << here !!
        }
        .padding(.horizontal)
    }
}

struct ViewHeightKey: PreferenceKey {
    static var defaultValue: CGFloat { 0 }
    static func reduce(value: inout Value, nextValue: () -> Value) {
        value = value + nextValue()
    }
}

Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
Asperi
  • 228,894
  • 20
  • 464
  • 690
  • What is the reason to set `.frame(minHeight: labelHeight)` instead of `.frame(height: labelHeight)`? – Nace Jun 18 '20 at 19:21
  • 2
    I have seen you in, at least, on more post using Preference Key. I really do not understand why you are making things even hard for other developers. What you did here will mislead lost of beginners including myself. Please, do not answer just for the sake of answering. if you are not sure about something let the question "rot", I am quite sure someone will find a proper solution to problem without ugly workarounds. You can check the correct answer below. https://stackoverflow.com/a/75034970/5636313 – Farid Jan 06 '23 at 18:49
6

This is the answer without workaround.

struct DynamicallyScalingView: View {
    var body: some View {
        HStack(spacing: 20) {
            Image(systemName: "snow")
                .frame(maxHeight: .infinity)          // Add this
                .font(.system(size: 32))
                .padding(20)
                .background(Color.red.opacity(0.4))
                .cornerRadius(8)

            VStack(alignment: .leading, spacing: 8) {
                Text("My Title")
                    .foregroundColor(.white)
                    .font(.system(size: 13))
                    .padding(5)
                    .background(Color.black)
                    .cornerRadius(8)
                Text("Dynamic text that can be of different leghts. Spanning from one to multiple lines. When it's multiple lines, the background on the left should scale vertically")
                    .font(.system(size: 13))
            }
            .frame(maxHeight: .infinity)              // Add this
        }
        .padding(.horizontal)
        .fixedSize(horizontal: false, vertical: true) // Add this
    }
}

enter image description here

Farid
  • 2,317
  • 1
  • 20
  • 34