Animatable Text

4 minute read

Animation is an important part of mobile application nowadays as it improves user experience. The truth is that they are not that difficult usually either as basic UI components come with an animation support, at least to some extent. We can get for free component repositioning, size changes or simple opacity animation. With little effort we can also easily get a transition animation towards any screen edge for example.

Yet, sometimes we strive for more complex animation that is not supported out of the box. Fortunately, there is quite a simple way how to achieve them. As one might expect in swift, there’s a protocol just to implement.

Default Text animation

Before we even begin with animation itself, let’s see the default behaviour of Text component. Below is the code we’ll be using for this entire example. Interesting thing is that it’s not going to change any further, except for the Text component replacement but we’ll get to that later.

struct ContentView: View {
    @State var value: Int = 1

    var body: some View {
        Text("\(value)")
            .frame(width: 150, height: 150)
            .contentShape(Rectangle())
            .onTapGesture {
                withAnimation {
                    value = .random(in: 1...5000)
                }
            }
    }
}

When you run the above code, this is the result you’ll get. Integer value changes on each component tap.

Final tag component layout example
Default text component animation

But the animation is not that great, old value simply fades out and new value fades in simultaneously. Even thought the value animates, it’s not much different from not having any animation at all. We can definitely do better given that we only display numeric values.

Animatable protocol

So we would like to customize the animation between two numeric values in the same text view. There is a protocol that can help us just with that - Animatable. It’s a simple protocol with one property requirement var animatableData: Self.AnimatableData. Notice of the generic type that allows you to animate changes of any value conforming to VectorArithmetic protocol (by default they’re Double, Float and CGFloat).

As we intend to display integer values only and Int type does not conform to VectorArithmetic by default, we have two options here. Either to conform to that protocol ourselves (through extension) or to convert the data internally to one of those supported data types. Both options work the same so in this example we’ll go with the second approach.

Implementation

All we need to do is to create a custom component wrapping Text view, make it conform to Animatable protocol and implement animatableData property that just converts our value to Double.

struct NumberText: View, Animatable {
    var value: Int

    var animatableData: Double {
        get { Double(value) }
        set { value = Int(newValue) }
    }

    var body: some View {
        Text("\(value)")
    }
}

Running the code above should give you the following animation.

Final tag component layout example
Customized text component animation

It may slightly differ for you though, depending on your location. The reason is that now it uses your device locale setting in order to decide number style. This may not be always required behaviour, especially if your app support multiple languages. So we’ll try to define our custom formatting style to the numeric value.

Formatted text

Let’s first prepare some utility code that can make our next implementation a lot easier. We’ll just need to create NumberFormatter instance with explicitly defined locale to override system locale and hardcoded numberStyle. Feel free to change numberStyle in your code as needed, could be also passed as parameter.

extension NumberFormatter {
    convenience init(locale: Locale) {
        self.init()
        self.locale = locale
        self.numberStyle = .decimal
    }
}

So next we’ll create a new View component FormattedNumberText that will implement Animatable protocol the same way as NumberText did. Plus we put a number formatter in place. This is the implementation we might end up with.

struct FormattedNumberText: View, Animatable {
    var value: Int
    let formatter: NumberFormatter

    static var defaultFormatter = NumberFormatter(locale: .current)

    init(value: Int, formatter: NumberFormatter = Self.defaultFormatter) {
        self.value = value
        self.formatter = formatter
    }

    init(value: Int, locale: Locale) {
        self.value = value
        self.formatter = NumberFormatter(locale: locale)
    }

    var animatableData: Double {
        get { Double(value) }
        set { value = Int(newValue) }
    }

    var body: some View {
        Text(NSNumber(integerLiteral: value), formatter: formatter)
    }
}

You can now use this new component in the original ContentView code by replacing Text("\(value)") with FormattedNumberText(value: value). By doing so, we’ll still get the same result as we did previously.

However, we can now also pass locale value to override default number style. For example, if your locale is en_US, then a value of 1000 will be formatted as 1,000. The same value for locale cs_CZ is displayed as 1 000. You can see yourself by passing the locale of your choice here.

struct ContentView: View {
    @State var value: Int = 1

    var body: some View {
        FormattedNumberText(value: value, locale: Locale(identifier: "cs_CZ"))
            .frame(width: 150, height: 150)
            .contentShape(Rectangle())
            .onTapGesture {
                withAnimation {
                    value = .random(in: 1...5000)
                }
            }
    }
}

And that’s it. You’re now able to override number formatting style by using proper Locale instance for your users. And the final animation still works the same.

Final tag component layout example
Customized text component animation with custom Locale

Summary

SwiftUI is quite capable of providing simple animation for basic components, however they’re not always what one might hope for. For those cases, there is Animatable protocol to the rescue. By implementing single variable we can help SwiftUI to behave the way we’d like to.

I’m sure that you’ll find yourself pretty soon how easy it can be once you’ll try one or two implementations. And the final impact on user experience is definitely worth it.

All code is available in the repository.

Updated: