visit
Now it is an open-source project that is available as Cocoapod and Swift package:
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.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 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.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
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
}()
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: . Welcome to everyone who can contribute and suggest improvements to this project via PR!