I\'d like a way of creating a local native-iOS time-hack proof background count-down timer, for example for counting down times for next reward in games.
Ex
I had these exact same requirements for an app I'm building. I tried using CACurrentMediaTime( ) but when you shut down the device it resets to 0 which isn't what you want. What worked even when shutting down the device was to use this time reference:
currentUser.timeLastUpdated = [[NSDate date] timeIntervalSince1970];
Here's the code to help you implement the timer. This is modified from another stackoverflow thread.
In the MainViewController.m
@interface MainViewController ()
@property (strong, nonatomic) IBOutlet UILabel *countdownLabel;
@end
@implementation MainViewController
{
UserData *_currentUser;
int hours;
int minutes;
int seconds;
}
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
_currentUser = [UserData currentUser];
[self updateTimerText];
[self startTimer];
}
- (void)startTimer {
[NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(updateTimer:) userInfo:nil repeats:YES];
}
- (void)updateTimer:(NSTimer *)timer {
if (_currentUser.secondsLeft > 0) {
_currentUser.secondsLeft--;
[self updateTimerText];
}
}
- (void)updateTimerText {
int secondsLeft = _currentUser.secondsLeft;
hours = secondsLeft / 3600;
minutes = (secondsLeft%3600) / 60;
seconds = (secondsLeft%3600) % 60;
self.countdownLabel.text = [NSString stringWithFormat:@"%02d:%02d:%02d",hours,minutes,seconds];
}
}
Now to make it bulletproof so it doesn't stop counting when you stop the app, you need to modify AppDelegate.m.
In the AppDelegate.m
@implementation AppDelegate
{
UserData *currentUser;
}
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// Override point for customization after application launch.
// Intialize currentUser
currentUser = [UserData currentUser];
// Reset secondsLeft based on lastTimeUpdated
if (currentUser.secondsLeft > 0 && currentUser.lastTimeUpdated != 0) {
currentUser.secondsLeft -= ((int)[[NSDate date] timeIntervalSince1970]) - currentUser.lastTimeUpdated;
}
return YES;
}
- (void)applicationDidEnterBackground:(UIApplication *)application {
// Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
// If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
// Set the last time updated (only need to call it here because this is coupled with applicationWillTerminate
currentUser.lastTimeUpdated = (int)[[NSDate date] timeIntervalSince1970];
}
- (void)applicationWillEnterForeground:(UIApplication *)application {
// Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background.
// Reset secondsLeft based on lastTimeUpdated
if (currentUser.secondsLeft > 0 && currentUser.lastTimeUpdated != 0) {
currentUser.secondsLeft -= ((int)[[NSDate date] timeIntervalSince1970]) - currentUser.lastTimeUpdated;
}
}
You only need to modify three of the methods in the AppDelegate because ApplicationWillTerminate always fires with ApplicationDidEnterBackground (as far as I can tell).
Bother currentUser.secondsLeft and currentUser.lastTimeUpdated are ints. I casted [[NSDate date] timeIntervalSince1970] as an int because it returns a CFTimeInterval which is a typealias of double.
Finally, you have to make sure to save it to a database. I lazily save these two variables to NSUserDefaults whenever they're updated by including the following code in my UserData.m file:
In the UserData.m
-(void)setSecondsLeft:(int)secondsLeft {
_secondsLeft = secondsLeft;
[[NSUserDefaults standardUserDefaults] setObject:@(secondsLeft) forKey:@"secondsLeft"];
}
-(void)setLastTimeUpdated:(int)lastTimeUpdated {
_lastTimeUpdated = lastTimeUpdated;
[[NSUserDefaults standardUserDefaults] setObject:@(lastTimeUpdated) forKey:@"lastTimeUpdated"];
That way when the application quits the data is saved for later use.
You could use the current system uptime (mach_absolute_time()
). This function is easily accessible using CACurrentMediaTime()
which returns it handily in seconds. Then you probably have to set up some kind of interval to check this value. This can of course be done using NSDate since it's not a problem that it gets triggered by the user setting the clock forward. When it comes to restarts, simply save the start value and use that as an offset after reboot.
The only drawback is that it doesn't count the time the device is turned off but that's generally not a big deal these days when no one turns of their phones.
To get the actual timer function, simply save the value at the start and then check it regularly against the projected end value, perhaps with increasing resolutions as the end approaches.