Reputation: 10398
I have a label showing a number and I want to change it to a higher number, however I'd like to add a bit of flare to it. I'd like to have the number increment up to the higher number with an ease inout curve so it speeds up then slows down. This answer shows how to make it increment (the 2nd answer, not the accepted answer) but I'd rather animate it so I could also make it increase in size slightly then shrink again as well as the ease inout curve. how to do a running score animation in iphone sdk
Any ideas how best to achieve this? Thanks
The start/end numbers will be user inputted and I want it to increment up the the end number in the same amount of time. So if I have start 10 end 100 or start 10 end 1000 I want it to count up to the end number in say 5 seconds.
Upvotes: 47
Views: 43625
Reputation:
My variation of @Sergio's answer:
extension UILabel {
func countAnimation(upto: Double) {
let fromString = text?.replacingOccurrences(of: "Score: ", with: "")
let from: Double = fromString?.replacingOccurrences(of: ",", with: ".")
.components(separatedBy: CharacterSet.init(charactersIn: "-0123456789.").inverted)
.first.flatMap { Double($0) } ?? 0.0
let duration = 0.4
let diff = upto - from
let rate = duration / diff
for item in 0...Int(diff) {
DispatchQueue.main.asyncAfter(deadline: .now() + rate * Double(item)) {
self.text = "Score: \(Int(from + diff * (Double(item) / diff)))"
}
}
}
}
So amount of steps is based on the difference between starting and ending values
Upvotes: 1
Reputation: 11257
Here @malex's answer in swift 3.
func incrementLabel(to endValue: Int) {
let duration: Double = 2.0 //seconds
DispatchQueue.global().async {
for i in 0 ..< (endValue + 1) {
let sleepTime = UInt32(duration/Double(endValue) * 1000000.0)
usleep(sleepTime)
DispatchQueue.main.async {
self.myLabel.text = "\(i)"
}
}
}
}
However, I strongly recommend simply downloading this class from GitHub and dragging it into your project, I've resorted to using it because the timing in my code doesn't seem to adjust properly for lower/higher count numbers. This class works great and looks very good. See this medium article for reference.
Upvotes: 21
Reputation: 3380
For the ones looking for a linear slow down counter (Swift 5) :
func animateCountLabel(to userCount: Int) {
countLabel.text = " "
let countStart: Int = userCount - 10
let countEnd: Int = userCount
DispatchQueue.global(qos: .default).async { [weak self] in
for i in (countStart...countEnd) {
let delayTime = UInt64(pow(2, (Float(i) - Float(countStart))))
usleep(useconds_t( delayTime * 1300 )) // sleep in microseconds
DispatchQueue.main.async { [weak self] in
self?.countLabel.text = "\(i)"
}
}
}
}
You can change the start end points and slow down speed by changing static values to your needs ;)
Upvotes: 1
Reputation: 2473
There you have it without blocking sleeps! stick this to a UILabel and it counts up and down from the value currently displaying, cleaning non numbers first. Adjust from Double to Int or Float if needed.
yourlabel.countAnimation(upto: 100.0)
another simple alternative
extension UILabel {
func countAnimation(upto: Double) {
let from: Double = text?.replace(string: ",", replacement: ".").components(separatedBy: CharacterSet.init(charactersIn: "-0123456789.").inverted).first.flatMap { Double($0) } ?? 0.0
let steps: Int = 20
let duration = 0.350
let rate = duration / Double(steps)
let diff = upto - from
for i in 0...steps {
DispatchQueue.main.asyncAfter(deadline: .now() + rate * Double(i)) {
self.text = "\(from + diff * (Double(i) / Double(steps)))"
}
}
}
}
Upvotes: 3
Reputation: 11
If you want a fast counting animation, you can use a recursive func like so:
func updateGems(diff: Int) {
animateIncrement(diff: diff, index: 0, start: 50)
}
func animateIncrement(diff: Int, index: Int, start: Int) {
if index == diff {return}
gemsLabel.text = "\(start + index)"
DispatchQueue.main.asyncAfter(deadline: .now() + 0.002) {
self.animateIncrement(diff: diff, index: index + 1, start: start)
}
}
Upvotes: 1
Reputation: 811
Swift 4 Code :
let animationPeriod: Float = 1
DispatchQueue.global(qos: .default).async(execute: {
for i in 1..<Int(endValue) {
usleep(useconds_t(animationPeriod / 10 * 10000)) // sleep in microseconds
DispatchQueue.main.async(execute: {
self.lbl.text = "\(i+1)"
})
}
})
Upvotes: 1
Reputation: 25294
Xcode 9.2, swift 4
class LoadingProcess {
let minValue: Int
let maxValue: Int
var currentValue: Int
private let progressQueue = DispatchQueue(label: "ProgressView")
private let semaphore = DispatchSemaphore(value: 1)
init (minValue: Int, maxValue: Int) {
self.minValue = minValue
self.currentValue = minValue
self.maxValue = maxValue
}
private func delay(stepDelayUsec: useconds_t, completion: @escaping ()->()) {
usleep(stepDelayUsec)
DispatchQueue.main.async {
completion()
}
}
func simulateLoading(toValue: Int, step: Int = 1, stepDelayUsec: useconds_t? = 10_000,
valueChanged: @escaping (_ currentValue: Int)->(),
completion: ((_ currentValue: Int)->())? = nil) {
semaphore.wait()
progressQueue.sync {
if currentValue <= toValue && currentValue <= maxValue {
usleep(stepDelayUsec!)
DispatchQueue.main.async {
valueChanged(self.currentValue)
self.currentValue += step
self.semaphore.signal()
self.simulateLoading(toValue: toValue, step: step, stepDelayUsec: stepDelayUsec, valueChanged: valueChanged, completion: completion)
}
} else {
self.semaphore.signal()
completion?(currentValue)
}
}
}
func finish(step: Int = 1, stepDelayUsec: useconds_t? = 10_000,
valueChanged: @escaping (_ currentValue: Int)->(),
completion: ((_ currentValue: Int)->())? = nil) {
simulateLoading(toValue: maxValue, step: step, stepDelayUsec: stepDelayUsec, valueChanged: valueChanged, completion: completion)
}
}
let loadingProcess = LoadingProcess(minValue: 0, maxValue: 100)
loadingProcess.simulateLoading(toValue: 80, valueChanged: { currentValue in
// Update views
})
DispatchQueue.global(qos: .background).async {
print("Start loading data")
sleep(5)
print("Data loaded")
loadingProcess.finish(valueChanged: { currentValue in
// Update views
}) { _ in
print("End")
}
}
Do not forget to add the solution code here
import UIKit
class ViewController: UIViewController {
weak var counterLabel: UILabel!
weak var progressView: UIProgressView!
override func viewDidLoad() {
super.viewDidLoad()
let stackView = UIStackView()
stackView.axis = .vertical
stackView.alignment = .fill
stackView.distribution = .fillProportionally
stackView.spacing = 8
stackView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(stackView)
stackView.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
stackView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 80).isActive = true
stackView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -80).isActive = true
let label = UILabel()
label.textAlignment = .center
label.text = "0"
label.font = UIFont.systemFont(ofSize: 46)
stackView.addArrangedSubview(label)
counterLabel = label
let progressView = UIProgressView()
progressView.trackTintColor = .lightGray
progressView.progressTintColor = .blue
progressView.layer.cornerRadius = 4
progressView.clipsToBounds = true
progressView.heightAnchor.constraint(equalToConstant: 8).isActive = true
stackView.addArrangedSubview(progressView)
self.progressView = progressView
let button = UIButton()
button.setTitle("Start", for: .normal)
button.addTarget(self, action: #selector(startButtonTapped), for: .touchUpInside)
button.setTitleColor(.blue, for: .normal)
button.heightAnchor.constraint(equalToConstant: 30).isActive = true
stackView.addArrangedSubview(button)
}
@objc func startButtonTapped() {
sample()
}
private func setProcess(currentValue: Int) {
let value = 0.01 * Float(currentValue)
self.counterLabel?.text = "\(currentValue)"
self.progressView?.setProgress(value, animated: true)
print("\(currentValue)")
}
func sample() {
let loadingProcess = LoadingProcess(minValue: 0, maxValue: 100)
loadingProcess.simulateLoading(toValue: 80, valueChanged: { currentValue in
self.setProcess(currentValue: currentValue)
})
DispatchQueue.global(qos: .background).async {
print("Start loading data")
sleep(5)
print("Data loaded")
loadingProcess.finish(valueChanged: { currentValue in
self.setProcess(currentValue: currentValue)
}) { _ in
print("end")
}
}
}
}
Upvotes: 0
Reputation: 10346
You can also check https://github.com/leszek-s/LSCategories
It allows incrementing/decrementing number in UILabel with a single line of code like this:
[self.label lsAnimateCounterWithStartValue:10 endValue:100 duration:5 completionBlock:nil];
Upvotes: 0
Reputation: 791
This is how i did it :
- (void)setupAndStartCounter:(CGFloat)duration {
NSUInteger step = 3;//use your own logic here to define the step.
NSUInteger remainder = numberYouWantToCount%step;//for me it was 30
if (step < remainder) {
remainder = remainder%step;
}
self.aTimer = [self getTimer:[self getInvocation:@selector(animateLabel:increment:) arguments:[NSMutableArray arrayWithObjects:[NSNumber numberWithInteger:remainder], [NSNumber numberWithInteger:step], nil]] timeInterval:(duration * (step/(float) numberYouWantToCountTo)) willRepeat:YES];
[self addTimerToRunLoop:self.aTimer];
}
- (void)animateLabel:(NSNumber*)remainder increment:(NSNumber*)increment {
NSInteger finish = finalValue;
if ([self.aLabel.text integerValue] <= (finish) && ([self.aLabel.text integerValue] + [increment integerValue]<= (finish))) {
self.aLabel.text = [NSString stringWithFormat:@"%lu",(unsigned long)([self.aLabel.text integerValue] + [increment integerValue])];
}else{
self.aLabel.text = [NSString stringWithFormat:@"%lu",(unsigned long)([self.aLabel.text integerValue] + [remainder integerValue])];
[self.aTimer invalidate];
self.aTimer = nil;
}
}
#pragma mark -
#pragma mark Timer related Functions
- (NSTimer*)getTimer:(NSInvocation *)invocation timeInterval:(NSTimeInterval)timeInterval willRepeat:(BOOL)willRepeat
{
return [NSTimer timerWithTimeInterval:timeInterval invocation:invocation repeats:willRepeat];
}
- (NSInvocation*)getInvocation:(SEL)methodName arguments:(NSMutableArray*)arguments
{
NSMethodSignature *sig = [self methodSignatureForSelector:methodName];
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:sig];
[invocation setTarget:self];
[invocation setSelector:methodName];
if (arguments != nil)
{
id arg1 = [arguments objectAtIndex:0];
id arg2 = [arguments objectAtIndex:1];
[invocation setArgument:&arg1 atIndex:2];
[invocation setArgument:&arg2 atIndex:3];
}
return invocation;
}
- (void)addTimerToRunLoop:(NSTimer*)timer
{
[[NSRunLoop mainRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
}
Upvotes: 0
Reputation: 10096
You can use GCD to shift delays to background threads.
Here is the example of value animation (from 1 to 100 in 10 seconds)
float animationPeriod = 10;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
for (int i = 1; i < 101; i ++) {
usleep(animationPeriod/100 * 1000000); // sleep in microseconds
dispatch_async(dispatch_get_main_queue(), ^{
yourLabel.text = [NSString stringWithFormat:@"%d", i];
});
}
});
Upvotes: 18
Reputation: 14446
I actually made such a class just for this called UICountingLabel:
http://github.com/dataxpress/UICountingLabel
It allows you to specify whether you want the counting mode to be linear, ease in, ease out, or ease in/out. Ease in/out starts counting slowly, speeds up, and then finishes slowly - all in whatever amount of time you specify.
It doesn't currently support setting the actual font size of the label based on the current value, though I may add support for that if it's a feature that's in-demand. Most of the labels in my layouts don't have a lot of room to grow or shrink, so I'm not sure how you want to use it. However, it behaves totally like a normal label, so you can change the font size on your own as well.
Upvotes: 77
Reputation: 1251
You could use a flag to see if it has to go up or down. Instead of a for loop, use a while loop. In this way, you are creating a loop that keeps going, so you have to find a way to stop it also, f.e. by a button press.
Upvotes: 5