Reputation: 813
I have to support iOS 13 in SwiftUI and I have to implement a view like on the image.
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:
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
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