I am trying to animate value change in a text using withAnimation
but it doesn't seem to work. I have come across a similar question but the answer is not animating the text value.
I am trying to recreate this behaviour in pure SwiftUI (UIKit Example):
I have tried this code but it doesn't animate the text change:
struct TextAnimationView: View {
@State private var textValue = "0"
var body: some View {
VStack (spacing: 50) {
Text(textValue)
.font(.largeTitle)
.frame(width: 200, height: 200)
.transition(.opacity)
Button("Next") {
withAnimation (.easeInOut(duration: 1)) {
self.textValue = "\(Int.random(in: 1...100))"
}
}
}
}
}
I have a very little experience with SwiftUI, is there another way to achieve this?
Thanks in advance :)
So it turns out this is really easy
Text(textValue)
.font(.largeTitle)
.frame(width: 200, height: 200)
.transition(.opacity)
.id("MyTitleComponent" + textValue)
Note the additional id
at the end. SwiftUI uses this to decide if it's dealing with the same view or not when doing a redraw. If the id is different then it assumes the previous view was removed and this one has been added. Because it's adding a new view it applies the specified transition as expected.
NB: It's quite possible that this id should be unique for the entire view tree so you probably want to take care to namespace it accordingly (hence the MyTitleComponent
prefix in the example).
I couldn't find a way to animate the text value with a fade. When setting the animation property of a Text
you will see three dots (...) when animating.
As for now I figured out a work around which will change the opacity:
@State private var textValue: Int = 1
@State private var opacity: Double = 1
var body: some View {
VStack (spacing: 50) {
Text("\(textValue)")
.font(.largeTitle)
.frame(width: 200, height: 200)
.opacity(opacity)
Button("Next") {
withAnimation(.easeInOut(duration: 0.5), {
self.opacity = 0
})
self.textValue += 1
withAnimation(.easeInOut(duration: 1), {
self.opacity = 1
})
}
}
}
This will fade out and fade in the text when you change it.
Below is an approach with AnimatableModifier
. It only fades in the new value. If you want to fade out the old value as well, it won't be that hard to customize the modifier. Also since your display value is numeric, you can use that itself as the control variable with some minor modifications.
This approach can be used for not only fade but also other types of animations, in response to a change in the value of the view. You can have additional arguments passed to the modifier. You may also completely ignore the passed content
in the body
and create and return an entirely new view. Overlay
, EmptyView
etc too can be handy in such cases.
import SwiftUI
struct FadeModifier: AnimatableModifier {
// To trigger the animation as well as to hold its final state
private let control: Bool
// SwiftUI gradually varies it from old value to the new value
var animatableData: Double = 0.0
// Re-created every time the control argument changes
init(control: Bool) {
// Set control to the new value
self.control = control
// Set animatableData to the new value. But SwiftUI again directly
// and gradually varies it from 0 to 1 or 1 to 0, while the body
// is being called to animate. Following line serves the purpose of
// associating the extenal control argument with the animatableData.
self.animatableData = control ? 1.0 : 0.0
}
// Called after each gradual change in animatableData to allow the
// modifier to animate
func body(content: Content) -> some View {
// content is the view on which .modifier is applied
content
// Map each "0 to 1" and "1 to 0" change to a "0 to 1" change
.opacity(control ? animatableData : 1.0 - animatableData)
// This modifier is animating the opacity by gradually setting
// incremental values. We don't want the system also to
// implicitly animate it each time we set it. It will also cancel
// out other implicit animations now present on the content.
.animation(nil)
}
}
struct ExampleView: View {
// Dummy control to trigger animation
@State var control: Bool = false
// Actual display value
@State var message: String = "Hi" {
didSet {
// Toggle the control to trigger a new fade animation
control.toggle()
}
}
var body: some View {
VStack {
Spacer()
Text(message)
.font(.largeTitle)
// Toggling the control causes the re-creation of FadeModifier()
// It is followed by a system managed gradual change in the
// animatableData from old value of control to new value. With
// each change in animatableData, the body() of FadeModifier is
// called, thus giving the effect of animation
.modifier(FadeModifier(control: control))
// Duration of the fade animation
.animation(.easeInOut(duration: 1.0))
Spacer()
Button(action: {
self.message = self.message == "Hi" ? "Hello" : "Hi"
}) {
Text("Change Text")
}
Spacer()
}
}
}
struct ExampleView_Previews: PreviewProvider {
static var previews: some View {
ExampleView()
}
}
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