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
.
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.
1final class NumberWheel: UIView {2 var value: Int? {3 didSet {4 guard let newValue = value else {5 return6 }78 // Only animate when we are transitioning9 if let oldValue = oldValue, oldValue != newValue {10 animateChange(old: oldValue, new: newValue)11 }12 }13 }1415 // Convert labels to images to make our lives easier16 lazy var numberLabels: [UIImageView] = {17 return (0...9).map {18 let label = UILabel(frame: self.frame)19 label.textAlignment = .center20 label.textColor = .black21 label.text = "\($0)"22 label.setFontSizeToFill()23 label.sizeToFit()24 let image = UIImage.imageWithLabel(label: label)25 return UIImageView(image: image)26 }27 }()2829 lazy var numberWheel: UIStackView = {30 let stackView = UIStackView(frame: self.frame)31 stackView.axis = .vertical32 stackView.spacing = 033 stackView.alignment = .center34 stackView.translatesAutoresizingMaskIntoConstraints = false35 numberLabels.forEach {36 stackView.addArrangedSubview($0)37 }38 return stackView39 }()4041 lazy var scrollView: UIScrollView = {42 let scrollView = UIScrollView(frame: self.frame)43 scrollView.translatesAutoresizingMaskIntoConstraints = false44 scrollView.addSubview(numberWheel)45 scrollView.showsHorizontalScrollIndicator = false46 scrollView.showsVerticalScrollIndicator = false47 scrollView.isUserInteractionEnabled = false48 numberWheel.pin(to: scrollView)49 numberWheel.widthAnchor.constraint(equalTo: scrollView.widthAnchor).isActive = true50 return scrollView51 }()5253 init(frame: CGRect, value: Int) {54 super.init(frame: frame)55 self.value = value5657 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.
1lazy var runningAnimator: UIViewPropertyAnimator = {2 // You can control these parameters to customize the feeling of the animation3 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.
1extension NumberWheel {2 func animateChange(old: Int, new: Int) {3 let destinationOffset = offset(of: new)45 // If there is an existing animation running, stop it6 if runningAnimator.isRunning {7 runningAnimator.stopAnimation(true)8 scrollView.contentOffset = self.scrollView.contentOffset9 }1011 // Add the new offset12 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.
1extension Dollar {2 /**3 Algorithm is `(1234 // (10 ** 2)) % 10`45 - Returns: nth element of `amount`, where `0`th element is the least significant digit6 - Note: Returns `0` if provided index is out of bounds7 */8 func digit(at i: Int) -> Int {9 if amount == 0.00 { return 0 }1011 let decimalsRemoved = Int(amount * 100)12 let divisionFactor = Int(pow(Double(10), Double(numberOfDigits - i - 1)))13 let movedToOne = decimalsRemoved / divisionFactor14 return movedToOne % 1015 }16}
With the above function, we can then create a NumberWheel
for every digit in a number to create a TickerLabel
.
1final class TickerLabel: UIView {2 var value: Dollar34 lazy var stackView: UIStackView = {5 let stackView = UIStackView(frame: frame)6 stackView.translatesAutoresizingMaskIntoConstraints = false7 stackView.axis = .horizontal8 stackView.spacing = 09 stackView.alignment = .fill10 return stackView11 }()1213 ...1415 func setupInitialLabels() {16 // 1. Create a label for each digit17 let numberLabels = (0..<value.numberOfDigits).map {18 value.digit(at: $0)19 }.map {20 NumberWheel(frame: self.approximateFrame, value: $0)21 }2223 // 2. Add each label to a stackView24 numberLabels.forEach {25 stackView.addArrangedSubview($0)26 }2728 // 3. Insert the necessary notations ("$", ".")29 insertNotations()3031 // 4. Add padding32 stackView.addArrangedSubview(UIView())33 }3435 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.
1extension TickerLabel {2 func animate(from old: Dollar, to new: Dollar) {3 // Note that the animation logic is essentially the same with the previously4 // implemented `NumberWheel`5 if runningAnimator.isRunning {6 runningAnimator.stopAnimation(true)7 }89 runningAnimator.addAnimations {10 (0..<new.numberOfDigits).forEach { i in11 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.
1var 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
Thanks to UIViewPropertyAnimator
and UIScrollView
, we were able to create a fully-interruptible, ticker label relatively easily! If you wish to download the full source of the label and use it in your own projects, please feel free to check out our Github repo.