I'm trying to add an expand/colapse animation on a Text
with multiple lines, and I'm having a strange behaviour.
Below is a gif with the issue. I've set slow animations to make it clear.
https://www.dropbox.com/s/sx41g9tfx4hd378/expand-collapse-stack_overflow.gif
I'm animating the height property of the view, and it seems that the Text
will convert immediately to one line disregarding the animation period. Here's some code:
struct ContentView: View {
@State var expanded = false
var body: some View {
VStack(spacing: 20) {
HStack {
Button(expanded ? "Colapse" : "Expand") {
withAnimation {
self.expanded.toggle()
}
}
}
VStack(spacing: 10) {
Text(bigText)
Text(bigText)
}
.frame(height: expanded ? .none : 0)
.clipped()
.background(Color.red)
Text("Thist is another text underneath the huge one. ")
.font(.system(.headline))
.foregroundColor(.red)
Spacer()
}
}
}
I have tried a lot of other things, and this is currently the closest to the desired output, which is the same as animating a label inside a UIStackView
in UIKit.
Is there a way to do this properly? Is this a bug?
Normally the problem is from the developer, but I noticed that if I use a DisclosureGroup
the animation works when it's expanding, but when it's collapsing it simply has no animation. So this might actually be a limitation of multi line Text
?
Thank you very much.
Using SwiftUI TextEditor for Multiline Text Input The first version of SwiftUI doesn’t come with a native UI component for multiline text field. For multiline input, you can wrap a UITextView from the UIKit framework and make it available to your SwiftUI project by adopting the UIViewRepresentable protocol.
To make a multiline text field that grows with the content, we specify axis: .vertical as an initializer argument. This will make a text field grow dynamically with the content as long as there is enough space. axis: .horizontal will yield the same result as a normal text field. A multiline text field.
The new version of SwiftUI introduces an onChange () modifier which can be attached to TextEditor or any other view. Let’s say, if you are building a note application using TextEditor and need to display a word count in real time, you can attach the onChange () modifier to TextEditor like this:
Well, the problem is that we animate frame between nil
and 0
, but to internal Text items only edge values are transferred.
To solve this it should be done two steps:
So here is a demo of approach. Prepared with Xcode 13 / iOS 15
Note: slow animation is activated in Simulator for better visibility
let bigText = """
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
"""
struct ContentView: View {
// we need `true` on construction (not visible) to calculate
// max content height
@State var expanded = true // << required initial true !!
@State private var maxHeight: CGFloat?
var body: some View {
VStack(spacing: 20) {
HStack {
Button(expanded ? "Colapse" : "Expand") {
withAnimation {
self.expanded.toggle()
}
}
}
VStack(spacing: 10) {
Text(bigText)
Text(bigText)
}
.background(GeometryReader { // read content height
Color.clear.preference(key: ViewHeightKey.self,
value: $0.frame(in: .local).size.height)
})
.onPreferenceChange(ViewHeightKey.self) {
if nil == self.maxHeight {
self.maxHeight = $0 // << needed once !!
}
}
.modifier(AnimatingFrameHeight(height: expanded ? maxHeight ?? .infinity : 0))
.clipped()
.background(Color.red)
Text("Thist is another text underneath the huge one. ")
.font(.system(.headline))
.foregroundColor(.red)
Spacer()
}
.onAppear {
// this cases instance redraw on first render
// so initial state will be invisible for user
expanded = false // << set if needed here !!
}
}
}
struct AnimatingFrameHeight: AnimatableModifier {
var height: CGFloat = 0
var animatableData: CGFloat {
get { height }
set { height = newValue }
}
func body(content: Content) -> some View {
content.frame(height: height)
}
}
struct ViewHeightKey: PreferenceKey {
static var defaultValue: CGFloat { 0 }
static func reduce(value: inout Value, nextValue: () -> Value) {
value = value + nextValue()
}
}
backup
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With