At Mosaic, our number one priority is being able to provide our users with the best-in-class user experience. We believe that while the key to providing users with an unforgettable experience is dependent on several factors, the presence of interactive, buttery smooth animations throughout the app is one of the most important. Today, we’d like to share some insight into how we made one of our favorite animations throughout the app — the ticker label that displays the change in monetary value.

As you see, the animation is fully interruptible, the transition has a nice springy feeling to it, and is just really fun to play with. We could have had a simple fade transition instead of putting in the man-hours to engineer this kind of animation. However, the end result of this implementation was that it drove user engagement up and added a playful feeling to an app that tries to deal with a rather serious topic — personal finances. So, how did we make this label?

How we did it

A quick note before we go on. We’ll be implementing the above ticker view in iOS. If you come from the Android world, we highly recommend you check out this blog post.

When we first decided to implement a ticker view in the app, we immediately knew which basic UIView components could be used to make the label. In addition, we thought that we would make the animation work for a single digit first, and then eventually build up to the full label with all the necessary digits. To make this process easier to follow, allow us to break the construction of the view into 2 major parts.

1. A NumberWheel

The NumberWheel is essentially a UIScrollView with a UIStackView containing labels of numbers from 0 ~ 9. The scroll view’s size will be the size of a single number label in order to only show it’s current value .

Here you can see a single NumberWheel with its value set to 3.

2. An array of NumberWheel s

To make the full view, we’ll use an UIStackView containing an array of NumberWheel s depending on the number of digits in the number. Note that the first element will always be a static $ and the second to last element in the stack view will always be a .

Cool! So we have a gist of how to implement this stuff. Now let’s look at some code :D

The Code

Let’s start off with the NumberWheel . As mentioned above, the wheel will consist of labels from 0 ~ 9. The labels will be contained in a vertical UIStackView , which will then be contained in a UIScrollView . For a more detailed explanation of how to add a stackView within a scroll view, we highly recommend you to check out this post.

Copy 1 final class NumberWheel : UIView { 2 var value : Int ? { 3 didSet { 4 guard let newValue = value else { 5 return 6 } 7 8 9 if let oldValue = oldValue , oldValue != newValue { 10 animateChange ( old : oldValue , new : newValue ) 11 } 12 } 13 } 14 15 16 lazy var numberLabels : [ UIImageView ] = { 17 return ( 0 . . . 9 ) . map { 18 let label = UILabel ( frame : self . frame ) 19 label . textAlignment = . center 20 label . textColor = . black 21 label . text = " \( $ 0 ) " 22 label . setFontSizeToFill ( ) 23 label . sizeToFit ( ) 24 let image = UIImage . imageWithLabel ( label : label ) 25 return UIImageView ( image : image ) 26 } 27 } ( ) 28 29 lazy var numberWheel : UIStackView = { 30 let stackView = UIStackView ( frame : self . frame ) 31 stackView . axis = . vertical 32 stackView . spacing = 0 33 stackView . alignment = . center 34 stackView . translatesAutoresizingMaskIntoConstraints = false 35 numberLabels . forEach { 36 stackView . addArrangedSubview ( $ 0 ) 37 } 38 return stackView 39 } ( ) 40 41 lazy var scrollView : UIScrollView = { 42 let scrollView = UIScrollView ( frame : self . frame ) 43 scrollView . translatesAutoresizingMaskIntoConstraints = false 44 scrollView . addSubview ( numberWheel ) 45 scrollView . showsHorizontalScrollIndicator = false 46 scrollView . showsVerticalScrollIndicator = false 47 scrollView . isUserInteractionEnabled = false 48 numberWheel . pin ( to : scrollView ) 49 numberWheel . widthAnchor . constraint ( equalTo : scrollView . widthAnchor ) . isActive = true 50 return scrollView 51 } ( ) 52 53 init ( frame : CGRect , value : Int ) { 54 super . init ( frame : frame ) 55 self . value = value 56 57 addSubview ( scrollView ) 58 scrollView . pin ( to : self ) 59 scrollView . setNeedsLayout ( ) 60 scrollView . layoutIfNeeded ( ) 61 scrollView . setContentOffset ( offset ( of : value ) , animated : false ) 62 } 63 }

The only thing to note here is that we converted all the labels into images. We this is to not worry about about text heights, offsets, insets, vertical spacing, and so on. In other words, we did it because it made our lives easier.

Now that we have the basic view setup, how can we go about animating the number transition? Well, since we chose to use a scrollView, we can thankfully use its scrollToOffset(animated:) function to animate things for now. Cool! So far, we can animate a single digit scrolling up and down. However, note that we can’t control the behavior of the animation nor the duration of the animation yet. Plus, we can’t interrupt the animation. To fix all these problems, we will need to use UIViewPropertyAnimators . If you need a refresher on this topic, feel free to check out this awesome WWDC video. Now that we are all animation experts, let’s try to make our animation more awesome-er.

The Animations

First off, let’s create a UIViewPropertyAnimator as all animations start and end with them.

Copy 1 lazy var runningAnimator : UIViewPropertyAnimator = { 2 3 UIViewPropertyAnimator ( duration : 0.5 , dampingRatio : 1.0 , animations : nil ) 4 } ( )

Now, let’s make a animateChange(old:, new:) function in NumberWheel that will contain the main animation logic.

Copy 1 extension NumberWheel { 2 func animateChange ( old : Int , new : Int ) { 3 let destinationOffset = offset ( of : new ) 4 5 6 if runningAnimator . isRunning { 7 runningAnimator . stopAnimation ( true ) 8 scrollView . contentOffset = self . scrollView . contentOffset 9 } 10 11 12 runningAnimator . addAnimations { 13 self . scrollView . setContentOffset ( destinationOffset , animated : true ) 14 } 15 runningAnimator . startAnimation ( ) 16 } 17 }

The most important part of the code is the predicate to see if the animator is running an existing animation or not. If there is a request to animate to a new offset while there is an existing animation going on, we will stop the animation and tell the scrollView to stop where it is right now. This allows for a smooth transition before adding a new animation to the animator. After adding the above function, we end up with something like this,

Now that we have the animation down for a single label, let’s go on to build the full thing.

The Dollar Label

Since we can create a fully animatable label for a single digit with, all we have to do now is to combine multiple of them into a single view. To do so, we can use the below function to get the nth digit in a number.

Copy 1 extension Dollar { 2 3 4 5 6 7 8 func digit ( at i : Int ) - > Int { 9 if amount == 0.00 { return 0 } 10 11 let decimalsRemoved = Int ( amount * 100 ) 12 let divisionFactor = Int ( pow ( Double ( 10 ) , Double ( numberOfDigits - i - 1 ) ) ) 13 let movedToOne = decimalsRemoved / divisionFactor 14 return movedToOne % 10 15 } 16 }

With the above function, we can then create a NumberWheel for every digit in a number to create a TickerLabel .

Copy 1 final class TickerLabel : UIView { 2 var value : Dollar 3 4 lazy var stackView : UIStackView = { 5 let stackView = UIStackView ( frame : frame ) 6 stackView . translatesAutoresizingMaskIntoConstraints = false 7 stackView . axis = . horizontal 8 stackView . spacing = 0 9 stackView . alignment = . fill 10 return stackView 11 } ( ) 12 13 . . . 14 15 func setupInitialLabels ( ) { 16 17 let numberLabels = ( 0 . . < value . numberOfDigits ) . map { 18 value . digit ( at : $ 0 ) 19 } . map { 20 NumberWheel ( frame : self . approximateFrame , value : $ 0 ) 21 } 22 23 24 numberLabels . forEach { 25 stackView . addArrangedSubview ( $ 0 ) 26 } 27 28 29 insertNotations ( ) 30 31 32 stackView . addArrangedSubview ( UIView ( ) ) 33 } 34 35 func insertNotations ( ) { 36 let dollarSign = staticLabel ( with : "$" ) 37 let period = staticLabel ( with : "." ) 38 stackView . insertArrangedSubview ( dollarSign , at : 0 ) 39 stackView . insertArrangedSubview ( period , at : stackView . arrangedSubviews . count - 2 ) 40 } 41 }

Once we have the initial labels set up, let’s use a UIViewPropertyAnimator to animate our changes.

Copy 1 extension TickerLabel { 2 func animate ( from old : Dollar , to new : Dollar ) { 3 4 5 if runningAnimator . isRunning { 6 runningAnimator . stopAnimation ( true ) 7 } 8 9 runningAnimator . addAnimations { 10 ( 0 . . < new . numberOfDigits ) . forEach { i in 11 let label = self . label ( at : i ) 12 label . value = new . digit ( at : i ) 13 } 14 } 15 runningAnimator . startAnimation ( ) 16 } 17 }

Lastly, let’s call the above animate function when the value of the TickerLabel is changed.

Copy 1 var value : Dollar { 2 didSet { 3 if oldValue != newValue { 4 animate ( from : oldValue , to : value ) 5 } 6 } 7 }

And there we have it! A fully-interruptable ticker label with buttery-smooth animations, that is really really fun to play with!

Conclusion