1

I have to support iOS 13 in SwiftUI and I have to implement a view like on the image.

enter image description here

When the user taps on the read more button, the view expands to show all the content. There is no read more button if the view can accommodate all the content.

How can I dynamically change the height when expanding/collapsing this view?

The api returns an array of text or list objects with styling information. I loop through them in a VStack{ ForEach { ... } } when I am building the general information view. I've attached the simplified code here for reference.

With the code below, this is what I have so far, when I collapse the view (limit the maxHeight), I get this:

enter image description hereenter image description here

See how the outer VStack (gray color) gets correctly resized, but the GeneralInformationView stays huge. I tried clipping it, but then it only shows the center of the text.


class ViewState: ObservableObject {
    @Published var isExpanded: Bool = false
    @Published var fullHeight: CGFloat = 0
}

struct ContentView: View {

    @ObservedObject var state: ViewState = ViewState()

    let maximumHeight: CGFloat = 200

    var showReadMoreButton: Bool {
        if state.isExpanded {
            return true
        } else {
            return state.fullHeight > maximumHeight
        }
    }

    var calculatedHeight: CGFloat {
        if !state.isExpanded && state.fullHeight > maximumHeight {
           return maximumHeight
       } else {
           return state.fullHeight
       }
    }

    var body: some View {
        VStack(spacing: 0) {
            GeneralInformationView()
                .background(GeometryReader { geometry in
                    Color.clear.preference(
                        key: HeightPreferenceKey.self,
                        value: geometry.size.height
                    )
                })
                .background(Color(.white))
                .frame(maxHeight: calculatedHeight)

            if showReadMoreButton {
                ReadMoreButton().environmentObject(state)
            }
        }
        .padding(.all, 16)
        .frame(maxWidth: .infinity, maxHeight: calculatedHeight + (showReadMoreButton ? 60 : 0) // 60 is the read more button size
        .onPreferenceChange(HeightPreferenceKey.self) {
            state.fullHeight = $0
        }
        .background(Color(.gray))
    }

    struct HeightPreferenceKey: PreferenceKey {
        static let defaultValue: CGFloat = 0

        static func reduce(value: inout CGFloat,
                           nextValue: () -> CGFloat) {
            value = max(value, nextValue())
        }
    }
}

struct GeneralInformationView: View {
    var body: some View {
        VStack(spacing: 8) {

            Text("I am a title and I could be of any length.")
                .fixedSize(horizontal: false, vertical: true)
                .frame(maxWidth: .infinity, alignment: .leading)

            Text("""
                I am a text view but actually a list with bulletpoints!
                - I can be of any size
                - I am received by API
                - I must not be trunkated!
                - If I don't fit into the outer view when collapsed,
                    then I should just be clipped, from the top of course
            """)
                .fixedSize(horizontal: false, vertical: true)
                .frame(maxWidth: .infinity, alignment: .leading)
                .multilineTextAlignment(.leading)

            Text("I am another text here.")
                .fixedSize(horizontal: false, vertical: true)
                .frame(maxWidth: .infinity, alignment: .leading)

            Text("I am a text and I could be of any length.")
                .fixedSize(horizontal: false, vertical: true)
                .frame(maxWidth: .infinity, alignment: .leading)

            Text("I am a text and I could be of any length.")
                .fixedSize(horizontal: false, vertical: true)
                .frame(maxWidth: .infinity, alignment: .leading)

            Text("I am a text and I could be of any length.")
                .fixedSize(horizontal: false, vertical: true)
                .frame(maxWidth: .infinity, alignment: .leading)
        }
    }
}

struct ReadMoreButton: View {

    @EnvironmentObject var state: ViewState

    var body: some View {
        Button(action: {
            state.isExpanded.toggle()
        }, label: {
            HStack {
                Text(state.isExpanded ? "Collapse" : "Read More")
            }
            .frame(maxWidth: .infinity, minHeight: 60, maxHeight: 60, alignment: .center)
                        .foregroundColor(Color(.red))
                        .background(Color(.white))
            }).overlay(Rectangle()
                        .foregroundColor(.clear)
                        .background(LinearGradient(
                            gradient: Gradient(colors: [.white.opacity(0),
(state.isExpanded ? .white.opacity(0) : .white.opacity(1))]),

                            startPoint: .top,
                            endPoint: .bottom))
                        .frame(height: 25)
                        .alignmentGuide(.top) { $0[.top] + 25 },
                     alignment: .top)
    }
}

Vodenjak
  • 806
  • 1
  • 13
  • 21
  • 1
    This needs a [Minimal, Reproducible Example](https://stackoverflow.com/help/minimal-reproducible-example). No where in your code have you shown what `state` or `GeneralInformationView ` is, so it is hard to interpret your intentions. However, preference keys read what the actual current height of a view is, so you can apply it somewhere else. It does not compute the height that a view may be with different data. – Yrb Dec 28 '21 at 15:49
  • 1
    Thanks, I added a minimal reproducible example! – Vodenjak Dec 31 '21 at 08:18
  • 1
    I have been playing with this for a few days. I have come to the conclusion that limiting it in this way given the MRE you posted is impossible. The closest I can come is getting the window to look like your desired picture, but the text spills out like your other pictures. Even if I solved this I was going to recommend that you put this in a ScrollView as it is more appropriate. What happens if the amount of information is larger than the screen? With your solution, even if it worked, the user could not view all of it. – Yrb Jan 02 '22 at 02:31
  • Thank you for trying. I wonder if there is another way one could approach to develop this design. – Vodenjak Jan 03 '22 at 09:19

1 Answers1

0

If it could help someone, I found a solution with the help of this answer. Wrapping the GeneralInformationView() into a disabled ScrollView, and using minHeight instead of maxHeight in the frame modifier seemed to do the trick!

 var body: some View {
    VStack(spacing: 0) {
            ScrollView { // Here adding a ScrollView
                GeneralInformationView()
                    .background(GeometryReader { geometry in
                        Color.clear.preference(
                            key: HeightPreferenceKey.self,
                            value: geometry.size.height
                        )
                    })
                    .background(Color(.white))
                    .frame(minHeight: calculatedHeight) // Here using minHeight instead of maxHeight
            }
            .disabled(true) // Which is disabled

        if showReadMoreButton {
            ReadMoreButton().environmentObject(state)
        }

    }
// The rest is the same
Vodenjak
  • 806
  • 1
  • 13
  • 21