Learn how to create a SwiftUI “rolling number” animation that will amaze your users

Overview
Have you ever seen one of those cool animations where the numbers count really fast in an app/game? Maybe you’re XP went up 100 or your bank account just had a deposit so the numbers slowly counted and increased and it was a really satisfying feeling. Or maybe you went to Ruth’s Chris and you got a $100 steak but didn’t feel the real pain until you checked your credit card app and you saw the dreaded red number increase a whole Benjamin?
Either way, the micro-interaction that the app used, gave you some kind of emotional attachment (good or bad) that made you remember that feeling. I know whenever I pay off my credit card bill, there is this really satisfying rolling number and I can visually see my credit limit increase. Every-time I see that animation, I’m instantly happy.
So, I decided to bring that happiness to you! YES, YOU! By creating a tutorial for how to add this awesome animation to your apps. MY goal for this blog is to recreate this animation and give you a building-block for creating emotion-packed micro-interactions in your application. Once you’re done reading this, you will be able to recreate the animation seen below ⬇️

Isn’t that an awesome micro-interaction? The best part is that it’s extremely versatile for various apps. Sports apps, fighting games, financial games, poker, you name it! Let’s get started!
Animation Breakdown
Before we can create the animation, we need to understand what the animation is doing. When I first got into iOS development, I rushed into creating animations without understanding the physical elements that were going on within them.
I should’ve been asking myself, “how are you supposed to create something when you don’t know the basics of it?”.
Well, I’m going to make sure you don’t run into that same mistake that I did. Since the micro-interaction we’re recreating is really two animations happening at once, we’ll show you the elements you need to create both of them and then merge them back together in the end. So, let’s break down each of the animations step by step so we can recreate them in code!
Scale over animation (green/red number pop-up)
- The basic number that we’re showing initially
- Button click (or some event)
- The animated number starts scaling, from size 0, from directly behind the shown number
- The animated number moves vertically above the shown number (while still scaling)
- At its peak displacement, it has a spring movement and comes to rest
- Pauses for a second
- Then scales back down to it’s starting place where it cannot be seen
Rolling number animation
- The basic number that we’re showing initially
- Button click (or some event)
- Every fraction, add a fraction of the value being added to the base number and then display that intermediary number
- Repeat step 3 until the final sum is reached
Now that we have the steps needed to create our awesome micro-interaction, let’s start writing the code to achieve it!
Code the scaling animation
The scaling animation is relatively simple. All we need to do is create a string that comes from behind something else (think ZStack) and scales up to a larger size than when it started (scaleEffect). As it’s scaling, the text needs to move (offset) from its starting position to a y-value less than its current position (because the top left of the screen is (0,0)). We then slap a simple spring animation on top of all of that and we get the awesome animation that you saw in the preview.
struct ScalingNumberAnimation: View { @State var showScaledNumber = false var body: some View { VStack { ZStack { // object to animate Text("$1000") .font(.system(size: 18, weight: .medium, design: .serif)) .foregroundColor(.green) .offset(x: 0, y: self.showScaledNumber ? -25 : -10) .scaleEffect(self.showScaledNumber ? 1.25 : 0, anchor: .bottom) .animation(Animation.spring(dampingFraction: 0.7).speed(0.8)) // placeholder object Rectangle() .foregroundColor(.white) .frame(width: 100, height: 36) } Button(action: { self.showScaledNumber.toggle() }) { Text("Show Animation") .padding() .foregroundColor(.white) .background(Color.blue) .cornerRadius(10) } } } }
The most important lines of code to look at are the modifiers for the TextView
that is scaled. Notice that the y-offset
and scale factor
are variable because of the ternary operator. As a result, our view adapts it’s rendering when the @State
variable changes, meaning the animation will be created for us!
Lastly, add a spring animation modifier to the view. The spring animation will give the View
a little bounce when it gets to the top and will make it so it isn’t so stiff and linear 🤮. No-one likes stiff animations. Don’t be that developer making your users want to vomit!
Code the rolling number animation
Rolling number animations are a bit more complex than the scaling animation that we just created. To create the rolling number animation, we need to control time like we’re in the matrix.
I know what you’re probably thinking, “Brady, WTF do you mean control time?”. I’m not asking you to be Neo or anything, I’m just saying that we need to control when, in time, that actions take place on the screen. We can no longer just rely on a simple boolean “ON/OFF”. Instead, we now need to tell SwiftUI something like this, “Okay it’s been a fifth of a second, update the number for me”.
To do this, we’re going to use our trusty friend DispatchQueue
. The DispatchQueue
gives us an amazing function called asyncAfter
which will allow us to run some code after a deadline. We can then use this to our advantage to split up the number that we’re adding into chunks, and add it over small intervals in time.
By doing this, we will give the user an illusion of the number having a “rolling counting” animation. This animation will have your users saying, “Recount it” 💸 in no time…
struct RollingCounterAnimation { @State var enteredNumber = 10 @State var total = 0 var body: some View { Text("\(total)") .font(.system(size: 18, weight: .medium, design: .serif)) .foregroundColor(.green) } /// Creates a rolling animation while adding entered number func addNumberWithRollingAnimation() { withAnimation { // Decide on the number of animation steps let animationDuration = 1000 // milliseconds let steps = min(abs(self.enteredNumber), 100) let stepDuration = (animationDuration / steps) // add the remainder of our entered num from the steps total += self.enteredNumber % steps // For each step (0..<steps).forEach { step in // create the period of time when we want to update the number // I chose to run the animation over a second let updateTimeInterval = DispatchTimeInterval.milliseconds(step * stepDuration) let deadline = DispatchTime.now() + updateTimeInterval // tell dispatch queue to run task after the deadline DispatchQueue.main.asyncAfter(deadline: deadline) { // Add piece of the entire entered number to our total self.total += Int(self.enteredNumber / steps) } } } } }
Within the code above, we really follow the steps that are outlined in the beginning.
First, we split the animation duration (1 sec) into steps which at max is 100 steps (this keeps from the rendering happening so fast it looks like it immediately goes to the final number). We then create a stepDuration
from the animation duration / number of steps
. The resulting quotient gives us the period of time that a single step will last. We then multiply the stepDuration
by the step
to get the timeInterval
after .now()
which will be when our code for the given step
should run. (read that through one more time to make sure that is in your head because I know it’s a bit confusing!).
Lastly, we need to write the code that will be run after we wait for the given deadline
. The code that will be run is simply adding the “chunk” of the entered number to our total (this could be any number in your app). To get this “chunk”, we simply divide the entered number
by the number of steps
(also note that we take care of remainders at the beginning). Remember that when we update a @State
variable, the view will rerender to match the current value of the variable. As a result, the “new” total will then be visible on the screen for the stepDuration
. Since the duration is so short, the human eye will think it’s running a counting animation!
All we have left to do is to put it together!
Putting it all together
Up until now, we’ve created the two animations separate from each-other. We did this because they are both capable of being performed without one another, and it allowed us to wrap our head around the building blocks needed to accomplish the task. But for this blog, it was our goal to run them at the same time, so it’s time to go big or go home. Let’s check out what that code would look like!
struct RollingNumberAnimation: View { @State var total = 0 @State var enteredNumber = 10 @State var showAnimation = false var backgroundColor = Color.white var body: some View { VStack { Text("Transaction Animator") Capsule() .foregroundColor(.clear) // Animation counting number ZStack(alignment: .center) { // current transaction "price" Text("$\(enteredNumber)") .foregroundColor(enteredNunberIsPositive ? .green : .red) .offset(x: 0, y: self.showAnimation ? -25 : -10) .scaleEffect(self.showAnimation ? 1.25 : 0, anchor: .bottom) .animation(Animation.spring(dampingFraction: 0.7).speed(0.8)) Text("$\(total)") // create a barrier from the total and scaled animation .background(Rectangle().foregroundColor(self.backgroundColor)) } .font(.system(size: 18, weight: .medium, design: .serif)) Form { // transaction cost stepper Stepper(onIncrement: { guard self.isAnimationAllowed else { return } self.enteredNumber += 101 }, onDecrement: { guard self.isAnimationAllowed else { return } self.enteredNumber -= 101 }) { Text("$\(enteredNumber)") } .padding(.horizontal) // button to add transaction Button(action: { self.addNumberWithRollingAnimation()}) { Text("Submit Transaction") } } } } } extension RollingNumberAnimation { var enteredNunberIsPositive: Bool { enteredNumber > 0 // technically zero isn't positive lol } // guard against adding animation during another animation var isAnimationAllowed: Bool{ !self.showAnimation } func addNumberWithRollingAnimation() { guard isAnimationAllowed else { return } withAnimation { // show scale animation self.showAnimation = true // Decide on the number of animation steps let animationDuration = 1000 // milliseconds let steps = min(abs(self.enteredNumber), 100) let stepDuration = (animationDuration / steps) // add the remainder of our entered num from the steps self.total += self.enteredNumber % steps (0..<steps).forEach { step in let updateTimeInterval = DispatchTimeInterval.milliseconds(step * stepDuration) let deadline = DispatchTime.now() + updateTimeInterval DispatchQueue.main.asyncAfter(deadline: deadline) { // Add piece of the entire entered number to our total self.total += Int(self.enteredNumber / steps) } } // hide scale animation DispatchQueue.main.asyncAfter(deadline: .now() + 2) { self.showAnimation = false } } } }
In the code above I added some bells and whistles that I’ll let you check out. The big conceptual piece that I want you to notice is that the View
is no different than the View
s that we wrote before. We simply combined the animations from the views to create a complete micro-interaction! Just like that, you created an amazing interaction that your users will love.
Conclusion
We’ve seen how easy it can be to create awesome animations for your apps when you simply break down the animation into byte-sized chunks. Once you have those chunks, writing the code is easy as 123, and in an hour or less you have an awesome animation that your users will come love and hopefully become attached to!
Before you leave, I want to state that the code above is not perfect by any means. There are definitely some improvements that can be made to fit SWE principles, but that is going to be left as HW for you when you integrate it with your application. I also want you to try and tinker with the parameters for the spring animation. You will be amazed at how much a drastic change a small delta of 0.1 can make on the animation!
Let me know in the comments down below if you have any mico-interactions / animations that you love and I’ll try to recreate them live in a youtube special! 🍻
If you liked today’s content, make sure you subscribe to the newsletter down below and if you want to support my work @ unwrapped bytes, consider helping me out by buying me a coffee! Every dollar keeps me going to create more AWESOME FREE CONTENT FOR YOU! As always, thanks for taking the time to unwrap some bytes with me. Cheers!