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
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.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.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
.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
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
.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.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.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!