40

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

1
  • For those finding this in the future, take a look at the contentTransition API added in iOS 16. Commented Mar 15, 2024 at 15:55

5 Answers 5

109

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

Sign up to request clarification or add additional context in comments.

6 Comments

Very nice solution and explanation!
This is helpful when you have a custom view like MyView(data: self.model.currentData) and model.currentData is @Published. You change this is model and MyView changes, so you'd like a transition. Adding the .id() property tells SwiftUI this view is really a different view. THANK YOU!
For those who can't get it to work (no animation), try wrapping the Text in a Stack element (VStack, HStack, ZStack). Just wrapping in a Group didn't work in my case :(. Tested in Xcode 11.4. Any guesses why it works this way?
Does work. I'm programming for iOS 15, also using the wrapping in a stack element doesn't work.
Doesn't forcing a new view costly?
|
8

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

@State var myText = "Hello, world👋"

VStack {
    Text(myText)
        .transition(AnyTransition.opacity.combined(with: .scale))
        .id("text-component" + MyText)

    Button("Button") {
        withAnimation(.easeInOut(duration: 1.0)) {
            myText = "Vote for my post🤩"
        }
    }
}

Comments

6

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.

1 Comment

That's really interesting however I was looking for the "cross-fade" effect. Thanks for answering
6

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

Comments

2

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

4 Comments

can you elaborate why we are doing textValue % 2 == 0 and textValue % 2 == 1. Is it just for the animation effect?
transition is activated on view appear/disappear, so here we need one view appearing and one view disappearing... separation by odd/even is just specific for this numbers related task.
Number counter is a very specific case. If you want to use with any other kind of text, just use a boolean state and toggle it every time you change the text. also, you can replace second if with an else statement for simplicity :)
I have two questions. 1) Why does the new number not animate, while the old one does fade out? 2) Why does this behave differently if you use an else instead of a new conditional?

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.