How to Save your Game’s Data: Part 2/2

In the second part of this tutorial, you will be adding your own anti-cheat system as well as saving your game’s data into iCloud. By Marin Todorov.

Leave a rating/review
Save for later
Share
You are currently viewing page 3 of 4 of this article. Click here to view the first page.

Fetching Data from iCloud

So far, you have the code in place to update the iCloud key-value storage with your local data. But how about fetching data from iCloud and updating your local storage?

Lucky for you iCloud shouts out whenever there are pending changes. You can simply observe the NSUbiquitousKeyValueStoreDidChangeExternallyNotification notification and update your local storage accordingly.

Since you want to start listening for iCloud changes as soon as RWGameData instance is created, the best way to do that is with a custom init.

Add the following code underneath init.

- (instancetype)init
{
  self = [super init];
  if (self) {
    //1
    if([NSUbiquitousKeyValueStore defaultStore]) {
      
      //2
      [[NSNotificationCenter defaultCenter] addObserver:self
        selector:@selector(updateFromiCloud:)
        name:NSUbiquitousKeyValueStoreDidChangeExternallyNotification
        object:nil];
    }
  }
  return self;
}
Note: The following code will produce a warning as you haven’t created updateFromiCloud: yet … so stay calm … that’s coming next :]

This is a classic init with couple of code additions:

  1. You check whether the device has enabled iCloud.
  2. You look for a NSUbiquitousKeyValueStoreDidChangeExternallyNotification in case iCloud is enabled.

This ensures that every time a change comes in from iCloud, updateFromiCloud: will fire up. For example, if you have the game open on two devices and take a photo on one of them.

Now you’ll code the observer method. Add it to the class with few initial lines of code:

-(void)updateFromiCloud:(NSNotification*) notificationObject
{
    NSUbiquitousKeyValueStore *iCloudStore = [NSUbiquitousKeyValueStore defaultStore];
    long cloudHighScore = [iCloudStore doubleForKey: SSGameDataHighScoreKey];
    self.highScore = MAX(cloudHighScore, self.highScore);
    
    
}

You’re probably already super-familiar with this kind of code. You simply fetch the default store and you get the iCloud high score in cloudHighScore. In the end, you assign the highest score from the local and iCloud data to self.highScore.

High score is all set, so now onto the pilot’s photo. Add this code to the bottom of updateFromiCloud:

if ([iCloudStore objectForKey: SSGameDataPilotPhotoKey]) {
  NSData* imageData = [iCloudStore objectForKey: SSGameDataPilotPhotoKey];
  if (imageData) {
    _pilotPhoto = [[UIImage alloc] initWithData:imageData];
  }
}

When unarchiving data from the game data file, you check for a stored photo, and if there is one you fetch the persisted NSData and use it to create a UIImage. You update RWGameData and save the image as the current pilot photo.

Do you know why you used the backing instance variable _pilotPhoto to set the photo instead of setting directly the property self.pilotPhoto? Try to figure it out. When you’re ready, click below to expand the answer.

[spoiler title=”Solution”]If you set the photo via the property you’ll:

  • Trigger the custom setter
  • Which will in turn upload the photo to iCloud
  • Which will in turn fire an iCloud update for your app
  • Which will trigger an iCloud notification
  • Which will fire up updateFromiCloud
  • Which will set the photo via self.pilotPhoto
  • Which will trigger the custom property setter

… etc. … etc. … etc. … FOREVER!
Long story short – you don’t want to use the custom setter in this case, changing the instance variable directly is the way to go.[/spoiler]

Now you’ve done all the necessary updates. To make sure all of the changes are persisted, also add a call to save the game data file to the disc.

[self save];

So you’ve stored data to iCloud and fetched with no (hopefully) problems. Don’t run the game just yet though.

Since iCloud is accessible via network, access and changes distribution are not instantaneous. Sometimes you’ll start the game and a few seconds into the gameplay the new photo will show up. Therefore, you need a way to update the game data and the scene changes that come in during gameplay.

sgd_23_challenge

You’ve already done half of the work, because you receive a notification when changes come in from iCloud. You just need to fire another notification when you’ve updated the game data so that MyScene knows to update the UI too.

Open RWGameData.h, and under the import statement define your notification’s name constant:

static NSString* const SSGameDataUpdatedFromiCloud = @"SSGameDataUpdatedFromiCloud";

Naturally, you’ll fire the notification just after you update the game data from iCloud. Switch back to RWGameData.m and find updateFromiCloud:. Just add this line to the end of it:

[[NSNotificationCenter defaultCenter] postNotificationName: SSGameDataUpdatedFromiCloud object:nil];

postNotificationName:object: will emit a notification through NSNotificationCenter. All you need to do is catch it in your scene class and update the on-screen high score and pilot photo.

Now open MyScene.m and scroll towards the bottom of initWithSize:. Find the [self setupHUD]; line. This is the right place to start observing for notifications about changes to the HUD. Add below that line:

[[NSNotificationCenter defaultCenter] addObserver:self
  selector:@selector(didUpdateGameData:)
  name:SSGameDataUpdatedFromiCloud
  object:nil];

You add the scene class as an observer for the SSGameDataUpdatedFromiCloud notification and instruct NSNotificationCenter to fire up didUpdateGameData: if such a notification comes in. Looking good!

Being a good programmer, you’ll also want to remove the class from NSNotificationCenter when it is destroyed. Add a dealloc to the class implementation:

-(void)dealloc
{
  [[NSNotificationCenter defaultCenter] removeObserver:self name:SSGameDataUpdatedFromiCloud object:nil];
}

This should take care to keep NSNotificationCenter sane and in balance.

And now – the grand finale! The method that will update the scene in real time! Yes, that means as changes come in!

Add this fabulous piece of code to wrap up iCloud integration:

-(void)didUpdateGameData:(NSNotification*)n
{
    _highScore.text = [NSString stringWithFormat:@"High: %li pt", [RWGameData sharedGameData].highScore];
    [self setupPilot];
}

Nothing much to discuss in there — you update the high score label and invoke setupPilot, which in turn updates the pilot photo.

That’s all there is to iCloud integration.

Now blow up some asteroids to celebrate your success!

sgd_24_victory

To test whether iCloud is working, try this on your iCloud enabled iOS device:

  1. Launch the game and take a new photo, the play the game few times
  2. Then stop the game from Xcode
  3. Delete the game from your device
  4. Now re-launch the game from within Xcode

So what happens when you launch the game after you deleted the previous installation?

You start with a clean slate:

sgd_25_cleanslate

A few seconds into the game:

sgd_26_cleanslate2

You got it working! Congrats! Time for a cold beer. :]

Contributors

Over 300 content creators. Join our team.