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)
    }
}

4
  • 1
    This needs a 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. Commented Dec 28, 2021 at 15:49
  • 1
    Thanks, I added a minimal reproducible example! Commented Dec 31, 2021 at 8: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. Commented Jan 2, 2022 at 2:31
  • Thank you for trying. I wonder if there is another way one could approach to develop this design. Commented Jan 3, 2022 at 9:19

1 Answer 1

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
Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.