Navigate back to the homepage

The Ticker

Mosaic Team
March 1st, 2021 · 3 min read

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.

GIF of final animation

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.

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

2. An array of NumberWheels

To make the full view, we’ll use an UIStackView containing an array of NumberWheels 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 return
6 }
7
8 // Only animate when we are transitioning
9 if let oldValue = oldValue, oldValue != newValue {
10 animateChange(old: oldValue, new: newValue)
11 }
12 }
13 }
14
15 // Convert labels to images to make our lives easier
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.

1lazy var runningAnimator: UIViewPropertyAnimator = {
2 // You can control these parameters to customize the feeling of the animation
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.

1extension NumberWheel {
2 func animateChange(old: Int, new: Int) {
3 let destinationOffset = offset(of: new)
4
5 // If there is an existing animation running, stop it
6 if runningAnimator.isRunning {
7 runningAnimator.stopAnimation(true)
8 scrollView.contentOffset = self.scrollView.contentOffset
9 }
10
11 // Add the new offset
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,

GIF of animation

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`
4
5 - Returns: nth element of `amount`, where `0`th element is the least significant digit
6 - Note: Returns `0` if provided index is out of bounds
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.

1final 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 // 1. Create a label for each digit
17 let numberLabels = (0..<value.numberOfDigits).map {
18 value.digit(at: $0)
19 }.map {
20 NumberWheel(frame: self.approximateFrame, value: $0)
21 }
22
23 // 2. Add each label to a stackView
24 numberLabels.forEach {
25 stackView.addArrangedSubview($0)
26 }
27
28 // 3. Insert the necessary notations ("$", ".")
29 insertNotations()
30
31 // 4. Add padding
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.

1extension TickerLabel {
2 func animate(from old: Dollar, to new: Dollar) {
3 // Note that the animation logic is essentially the same with the previously
4 // implemented `NumberWheel`
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.

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!

GIF of animation

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.

Wait!

If you enjoyed this post, we would love for you to check out Mosaic.

More articles from Mosaic Blog

Security & Privacy

How we keep your data secure and private

March 14th, 2021 · 1 min read

The Ticker

How we created a fully-animated ticker view in our new iOS app

March 1st, 2021 · 3 min read
© 2021 Mosaic Blog
Link to $https://twitter.com/mosaicmoneyLink to $https://www.instagram.com/mosaicmoney.app