How to animate/transition text value change in SwiftUI

14,075

Solution 1

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

Solution 2

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.

Solution 3

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

Solution 4

Here is the approach using standard transition. Font sizes, frames, animation durations are configurable up to your needs. Demo includes only important things for approach.

SwiftUI fade transition

struct TestFadeNumbers: View {
    @State private var textValue: Int = 0

    var body: some View {
        VStack (spacing: 50) {
            if textValue % 2 == 0 {
                Text("\(textValue)")
                    .font(.system(size: 200))
                    .transition(.opacity)
            }
            if textValue % 2 == 1 {
                Text("\(textValue)")
                    .font(.system(size: 200))
                    .transition(.opacity)
            }
            Button("Next") {
                withAnimation(.linear(duration: 0.25), {
                    self.textValue += 1
                })
            }
            Button("Reset") {
                withAnimation(.easeInOut(duration: 0.25), {
                    self.textValue = 0
                })
            }
        }
    }
}

Solution 5

If @Tobias Hesselink’s code doesn’t work, consider to use this:

@State var MyText = "Hello, world👋"

    VStack {
        Text(MyText)
            .transition(AnyTransition.opacity.combined(with: .scale))
            .id("MyTitleComponent" + MyText)
        Button("Button") {
            withAnimation(.easeInOut(duration: 1.0)) {
                MyText = "Vote for my post🤩"
            }}}
Share:
14,075
Umair M
Author by

Umair M

Flutter | iOS | .NET | Angular | Unity3D | Xamarin

Updated on June 15, 2022

Comments

  • Umair M
    Umair M almost 2 years

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

    enter image description here

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