Rolling Numbers Animation Using Only CALayers

Written by maxkalik | Published 2023/04/03
Tech Story Tags: swiftprogramming | calayer | ios-app-development | animation | numbers | cocoapods | calayers | rolling-numbers-animation

TLDRRolling Numbers is a lightweight UIView for getting smooth rolling animation between numbers implemented using only CALayer. Rolling Numbers is an open-source project that is available as Cocoapod and Swift package: https://github.com/maxkalik/RollingNumbersvia the TL;DR App

I’m an iOS Engineer at Triumph Labs where I develop TriumphSDK for game devs. Not so long ago I got a task to completely reimplement UI and animations. One of the most interesting parts was implementing of balance view with rolling animation of the number in it:
https://vimeo.com/813889559
At the first sight, it looks pretty simple but we needed a well-configured custom solution. Of course, I checked existing libraries that I could reuse and adjust this kind of animation for our needs. After the first try, I figured out that the libraries are not so adjustable and we cannot control the animation of each number. I won’t go into details too much, however, I decided to make my own solution from scratch.
Now it is an open-source project that is available as Cocoapod and Swift package: https://github.com/maxkalik/RollingNumbers

CALayers

I had a list of requirements — how should the animation look, but also I added another one — the component should be implemented only using 
CALayers
. Why? Most of the libraries that I checked were implemented using only 
UIViews
. The typical approach is making 
UIScrollViews
 or 
UIStackVeiws
 inside of a 
UIView
 where arranged views are UILabels. The rolling animation is implemented by using 
UIView.animate
 changing constraints. That’s absolutely ok but I wanted as much as possible to reduce the workload of performance.
CALayers represent the visual content of
UIView
. Layers provide low-level API of an efficient and more detailed configuration of rendering and animation using a low-cost Core Animation framework. Another important thing about Layers is that they are rendered using only GPU resources. This means the CPU will be free for another calculation task while the animation is running — that’s what I wanted to mention about performance.

Solution

If to check open-source solutions, the approach of how to construct the view is everywhere almost the same: a stack of numbers but visible ones only based on the state. My wireframe looks the same but different :) As you can see in the picture the stacks of numbers are doubled, this means the number of elements in one stack is 20.
This particular number of elements needs a smooth transition from an initial state to the next one and in the meantime the direction of rolling I wanted to be fully configurable. Let’s say, if a stack would have only 10 elements (and it would be pretty reasonable from the first sight) then the rolling direction from one digit to another would be always different. I needed to obey the direction rolling, for example, all digits from up to down.
Each stack in the view is 
CALayer
 where sublayers are numbers. The numbers are arranged by the same height, which means it’s easy to calculate the next position of the future number.

Characters

All characters are  
CATextLayers
and they have their own width. If to use 
UIStackView
 it’s not needed to care about width, all will be arranged by view configuration, but in my case — 
CALayers
don’t have such default implementation so it means it should be calculated.
To get the width of a character is a pretty straight-forward task using 
NSAttributedString
 and font:
private func prepareWidthOfChar(_ char: Character) -> Double {
    let fontAttributes = [NSAttributedString.Key.font: font]
    let size = String(char).size(withAttributes: fontAttributes as [NSAttributedString.Key : Any])
    return size.width * characterSpacing
}
This function also helps to calculate an actual with of all columns in the Rolling Numbers view. To get this information a developer can use the public property 
width
.

Animation Configuration

To achieve the spring animation effect I used 
CASpringAnimation
 changing 
y
 position of the
CALayer
 — a column where sublayers are numbers 
CATextLayers
.
private func animate(config: RollingNumbersView.AnimationConfiguration,
                 completion: (() -> Void)? = nil) {
        
  CATransaction.begin()
        
  let animation: CASpringAnimation = CASpringAnimation(keyPath: "position.y")
  let fromValue: CGFloat = position.y
  let toValue: CGFloat = moveToDigit()

  animation.fromValue = fromValue
  animation.toValue = toValue
  animation.duration = config.duration
  animation.speed = config.speed
  animation.damping = config.damping
  animation.initialVelocity = config.initialVelocity
  animation.isRemovedOnCompletion = false
  animation.fillMode = .forwards
        
  CATransaction.setCompletionBlock {
    completion?()
  }
        
  add(animation, forKey: nil)
        
  CATransaction.commit()
}
Additionally, I used 
CATransaction
 to catch the animation completion moment which reference I exposed in the Rolling Numbers view API as trailing completion. It happens only once after setting a new number with animation.
rollingNumbersView.setNumberWithAnimation(245699) {
    // completion
}
For the default animation config, I prepared a separate 
struct
 called: 
AnimationConfiguration
 . There are 4 initial configurations of the spring animation that are publically accessible.
duration: CFTimeInterval = 1,
speed: Float = 0.3,
damping: CGFloat = 17,
initialVelocity: CGFloat = 1

Animation Type

There are 4 animation types prepared for usage:
public enum AnimationType {
    case allNumbers
    case onlyChangedNumbers
    case allAfterFirstChangedNumber
    case noAnimation
}
AnimationType
 is accessible using public property 
animationType
 .
Let’s consider an example formatted as US currency. The initial value is $4.588.77. The future value is $4.576.67 (the changed numbers in the price I highlighted in bold).
By default, the animation is set up with 
.allAfterFirstChangedNumber
. This means if a future number is in the middle of the horizontal string then all others digits after this number will be animated. If to use 
.onlyChangedNumbers
 — this means literally: if a future number is different then only this number column will be scrolled. But somebody will need to roll all digits so for this you can use just 
.allNumbers
.

Rolling Direction

The numbers rolling direction can be configured using specific public properties 
rollingDirection
:
RollingDirectionwhere
obviously only two directions: 
.up
 and 
.down
. For example, if to set up this property as 
.up
 then the numbers in the view will always move in the up direction. However, initially, this property is nil which means by default the direction depends on a future number. If the future number will be less than the old one then the numbers column will move down and vice versa.

Formatting

There are several options to format the Rolling Numbers: alignment, character spacing, font, 
NumberFormatterconfigurator
, and text color.
var rollingNumbersView = {
    // Initialize Rolling Numbers view with initial number value
    let view = RollingNumbersView(number: 1234.56)
    
    // Spacing between numbers
    view.characterSpacing = 1
    
    // Text color
    view.textColor = .black
    
    // Alignment within UIView
    view.alignment = .left
    
    // UIFont
    view.font = .systemFont(ofSize: 48, weight: .medium)
    
    let formatter = NumberFormatter()
    formatter.numberStyle = .currency
    view.formatter = formatter
    
    return view
}()
Changing the size of the font keep in mind that the actual height of the view can be smaller, this means the digits can be invisible.
Besides general public property 
textColor
, there is another interesting public method:
rollingNumbersView.setTextColor(.blue, withAnimationDuration: 3)
Under the hood, the animation implemented using 
CABasicAnimation
 of 
forgroundColor
.The public 
setTextColor
 method helps to change the color of the text (numbers) while moving animation is happening! As an option changing color can be also an animation with duration.

Wrapping up

I enjoy building UI for mobile apps and solving performance problems. If it’s needed to use more than just 
UIView
 then Apple Documentation and tons of examples can help to make a more efficient solution.
When I finished building the Rolling Numbers component, I decided to share my solution with everyone — making a public library. Full documentation of usage of Rolling Number you can get from the GitHub repository: https://github.com/maxkalik/RollingNumbers. Welcome to everyone who can contribute and suggest improvements to this project via PR!

Links


Written by maxkalik | Senior iOS Engineer at Triumph Labs. Launched WordDeposit and SimpleRuler Apps. Tech writer and public speaker.
Published by HackerNoon on 2023/04/03