Vodenjak
Vodenjak

Reputation: 813

How to collapse/expand view based on dynamic content and maximum size restriction?

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

Upvotes: 1

Views: 831

Answers (1)

Vodenjak
Vodenjak

Reputation: 813

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

Upvotes: 0

Related Questions