Simple 3…2…1 countdown in iOS app I would like to display a 3...2...1 countdown on my iOS app (before to take a picture) but I didn't find an elegant solution. I don't want to make a Countdown app with an NSTimer. For me the important is to keep all code in the same method, use a system like Block.

self.infoLabel.text   = [NSString stringWithFormat:@"3"];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{

self.infoLabel.text = [NSString stringWithFormat:@"2"];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
    self.infoLabel.text = [NSString stringWithFormat:@"1"];

    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        self.infoLabel.hidden = YES;


            // DO WHAT I WANT AFTER 3 SECs
        });
    });
});
});


Do you have a better solution? t works okay, but it's not really elegant link up dispatch_after like I did.
The problem with any sort of timer in which each "tick" is a measurement from the previous "tick" is that in software, these "ticks" are not guaranteed to be exact.  This means that if your first tick is off by a tenth of a second, every remaining tick will be off by that amount or more.  Your second tick is scheduled based on your first tick.  This means the actual amount of total time passed is in exact, and the more ticks you have, the more error prone your total time is going to be.

So, for a start, I want to show you what a countdown object class might look like.  Then I'll walk through how we'd use this.

CRCountdown.h

typedef void (^CRCountdownCompletion)(void);

@interface CRCountdown : NSObject

- (void)startCountdownWithInterval:(NSTimeInterval)interval 
                             ticks:(NSUInteger)ticks 
                        completion:(CRCountdownCompletion)completion;

@property (readonly) NSUInteger ticksRemaining;
@property (readonly) NSTimeInterval interval;

@end


CRCountdown.m

@interface CRCountdown()

@property NSTimer *timer;
@property (readwrite) NSTimeInterval interval;
@property (copy) CRCountdownCompletion completion;

@end

@implementation CRCountdown

- (void)startCountdownWithInterval:(NSTimeInterval)interval ticks:(NSUInteger)ticks completion:(CRCountdownCompletion)completion {
    self.completion = completion;
    self.interval = interval;
    self.timer = [NSTimer scheduledTimerWithTimeInterval:(interval * ticks) target:self selector:@selector(countdownComplete:) userInfo:nil repeats:NO];
}

- (void)stopCountdown {
    [self.timer invalidate];
}

- (NSUInteger)ticksRemaining {
    if (self.timer.isValid) {
        NSTimeInterval timeRemaining = [self.timer.fireDate timeIntervalSinceDate:[NSDate date]];
        return timeRemaining / self.interval;
    } else {
        return 0;
    }
}

- (void)countdownComplete:(NSTimer *)timer {
    if (self.completion) {
        self.completion();
    }
}

@end




Now, this will guarantee that our completion event happens at as accurate a time as possible.  We still can't guarantee that it will happen precisely at a specific moment, but the margin of error is drastically reduced by eliminating the possibility of compounding the error for each one of our "update ticks".

Instead, we set up a repeating NSTimer that's in charge of calling some code to update the UI.  This UI update code can look at ticksRemaining (and if necessary, multiply this by interval) to determine what it should update the UI to represent.

It's not even necessary to pass anything for the completion argument, as our interested object will already be polling ticksRemaining, so whenever this returns 0, we know that the timer is complete (complete-ish anyway).

But we can go one step further with our Countdown object and provide a means for including an update block that is fired on each tick, if we want.  For this, however, we're going to want two NSTimer's ticking away.

First, in our header, let's add another block typedef:

typedef void (^CRCountdownUpdate)(NSUInteger);


We also need to change our startCountdown... method to take an argument of this type:

- (void)startCountdownWithInterval:(NSTimerInterval)interval
                             ticks:(NSUInteger)ticks
                            update:(CRCountdownUpdate)update
                        completion:(CRCountdownCompletion)completion;


This means adding two more properties in our .m, an updateTimer (NSTimer *) and a CRCountdownUpdate:

@property NSTimer *updateTimer;
@property (copy) CRCountdownUpdate update;


We need to modify our countdownComplete: method so it will invalidate the updateTimer (so update never fires after completion):

- (void)countdownComplete:(NSTimer *)timer {
    [self.updateTimer invalidate];
    if (self.completion) {
        self.completion();
    }
}


And add an update method to handle the update ticks:

- (void)countdownUpdate:(NSTimer *)timer {
    if (self.update) {
        self.update(self.ticksRemaining);
    }
}


The final step is modifying our startCountdown... method to match how we updated the header and to take care of setting up the update timer:

- (void)startCountdownWithInterval:(NSTimeInterval)interval ticks:(NSUInteger)ticks completion:(CRCountdownCompletion)completion update:(CRCountdownUpdate)update {
    self.completion = completion;
    self.update = update;
    self.interval = interval;
    self.timer = [NSTimer scheduledTimerWithTimeInterval:(interval * ticks) target:self selector:@selector(countdownComplete:) userInfo:nil repeats:NO];
    if (self.update) {
        self.updateTimer = [NSTimer scheduledTimerWithTimeInterval:interval target:self selector:@selector(countdownUpdate:) userInfo:nil repeats:YES];
    }
}




Now, using this object is as simple as this:

CRCountdownUpdate update = ^(NSUInteger ticks) {
    self.infoLabel.text = [NSString stringWithFormat:@"%lu", (unsigned long)ticks];
};

CRCountdownCompletion completion = ^{
    self.infoLabel.hidden = YES;
    // do whatever else you want
};

CRCountdown *countdown = [[CRCountdown alloc] init];
[countdown startCountdownWithInterval:1.0
                                ticks:3
                               update:update
                           completion:completion];




Final version of the class I'm suggesting looks like this:

CRCountdown.h

typedef void (^CRCountdownCompletion)(void);
typedef void (^CRCountdownUpdate)(NSUInteger);

@interface CRCountdown : NSObject

- (void)startCountdownWithInterval:(NSTimeInterval)interval ticks:(NSUInteger)ticks completion:(CRCountdownCompletion)completion update:(CRCountdownUpdate)update;
@property (readonly) NSUInteger ticksRemaining;
@property (readonly) NSTimeInterval interval;

@end


CRCountdown.m

@interface CRCountdown()

@property NSTimer *timer;
@property NSTimer *updateTimer;
@property (readwrite) NSTimeInterval interval;
@property (copy) CRCountdownCompletion completion;
@property (copy) CRCountdownUpdate update;

@end

@implementation CRCountdown

- (void)startCountdownWithInterval:(NSTimeInterval)interval ticks:(NSUInteger)ticks completion:(CRCountdownCompletion)completion update:(CRCountdownUpdate)update {
    self.completion = completion;
    self.update = update;
    self.interval = interval;
    self.timer = [NSTimer scheduledTimerWithTimeInterval:(interval * ticks) target:self selector:@selector(countdownComplete:) userInfo:nil repeats:NO];
    if (self.update) {
        self.updateTimer = [NSTimer scheduledTimerWithTimeInterval:interval target:self selector:@selector(countdownUpdate:) userInfo:nil repeats:YES];
    }
}

- (void)stopCountdown {
    [self.updateTimer invalidate];
    [self.timer invalidate];
}

- (NSUInteger)ticksRemaining {
    if (self.timer.isValid) {
        NSTimeInterval timeRemaining = [self.timer.fireDate timeIntervalSinceDate:[NSDate date]];
        return timeRemaining / self.interval;
    } else {
        return 0;
    }
}

- (void)countdownUpdate:(NSTimer *)timer {
    if (self.update) {
        self.update(self.ticksRemaining);
        NSString *s = [NSString stringWithFormat:@"%lu", (unsigned long)self.ticksRemaining];
    }
}

- (void)countdownComplete:(NSTimer *)timer {
    [self.updateTimer invalidate];
    if (self.completion) {
        self.completion();
    }

    self.update = nil;
    self.completion = nil;
}

@end


This of course, could also be done with delegation rather than blocks, which might be an appropriate approach.

I've created a github repo with both the block-based and delegation-based approaches.  It can be found here.