13

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.

Explanation of the object:

local
- Works without internet connection

native-iOS
- I want to find a solution in (preferably) Objective-C or Swift

time-hack proof
- When a user changes its device time forward / backward, remaining time stays the same

background
- Shut-down / re-open friendly

count-down timer
- A timer which, after start, will provide a method for checking the remaining time

I'd like to have a time-based reward system in my iOS game, which won't be easily hackable just by moving the device time forward.

Originally, I thought this was not possible and kept using [NSDate date] and savedDate, comparing them together to find elapsed time. Recently, however, I came across a game which uses this feature: Crossy Road. That is a Unity game, as far as I know, but I doubt that a somewhat common feature like this available in Unity project which is compiled into an iOS app is not accessible on iOS via Objective-C or Swift.

They provide a timed reward system, once per every 6 hours, and, as far as I tested, it's not hackable by changing the device time. It also works without an internet connection. (I'm not sure if it works if you don't ever connect, but when I connected, disconnected, and tried to use the hack, it didn't work.)

AstroCB
  • 12,337
  • 20
  • 57
  • 73
Gyfis
  • 1,174
  • 1
  • 14
  • 35
  • Set your own reference.. On load, save timestamp to database if not set (so record time of first run). Count time based on that reference point regardless of what time it actually is. – davidcondrey Jan 03 '15 at 09:31
  • How does this works against cheating? I understand saving the first timestamp to database, but what hack-proof method would work for getting 'current time' to get the difference? – Gyfis Jan 03 '15 at 09:52

2 Answers2

6

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.

Rick
  • 3,240
  • 2
  • 29
  • 53
  • I like this solution! But it would really be a lot better if you could count in the total time, with 'turned off' time too.. would you happen to know how? – Gyfis Dec 30 '14 at 17:57
  • @Gyfis have you tried turning off your phone while offline to see if the Unity game continues to progress time? It could be that this is the solution that they employed during offline play. – Stonz2 Dec 30 '14 at 19:57
  • Yes I tried that! And surprisingly, time in the Unity game was counted correctly even during the time my phone was turned off! Now it gets very interesting I guess! – Gyfis Dec 31 '14 at 09:31
  • That's a good question. Did you try going offline and then "cheat"? It could be that they simply sync the clock against a remote server with regular intervals. The device really only has two clocks, the uptime and the "real" time (Which can be changed by the user). – Rick Dec 31 '14 at 10:14
  • Another thing I just thought of, what about using the uptime and if it resets check the "real" clock for offset? That would make cheating a bit more convoluted at least, requiring a reboot, but also count the time when the device was offline. Of course it doesn't get rid of cheating completely but it might be a good compromise. – Rick Dec 31 '14 at 10:31
  • 1
    @Rick I think the suggested combination is as close as one gets, and I like it! Thanks for your answer and comments :) – Gyfis Jan 03 '15 at 09:57
  • @Rick I tried CACurrentMediaTime() but seems not accurate at long time (1 hour). And CFAbsoluteTimeGetCurrent() is modifiable by date change... – Simone Pistecchia Mar 24 '16 at 11:12
3

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.

Cœur
  • 37,241
  • 25
  • 195
  • 267
Rob Norback
  • 6,401
  • 2
  • 34
  • 38