iPhone - Saving game or app state using NSUserDefaults

In this article I'm going to explain how to store complex game or app data using the NSUserDefaults object in iOS. User defaults is ideal for storing structured information like: game state, high scores, game progress information. If you need to store unstructured information which you will be accessing by using SQL queries then you should use a built in database instead.

NSUserDefaults is the standard way of persisting data on the iPhone or iPad - much like Preferences API in Java. It stores the data in a .plist file in a safe directory. This is good for three reasons. First, your data will not be deleted when the app is updated. Second, your data will automatically be backed up during a synch with iTunes. Third, .plist is a tried and tested format which will not change. This means that an upgrade to iOS can't mess up your saved data. For more information look here.

Because user defaults uses a .plist file only certain data types are supported:

  • NSString
  • NSNumber
  • NSData
  • NSDate
  • NSArray
  • NSDictionary

As long as your data is stored in one of these formats it will be supported by NSUserDefaults. Lots of problems are caused by people trying to save incompatible objects. It is also possible to encode objects and store them in a NSData object as I'll show later.

Saving data with NSUserDefaults

Saving and retrieving data is very straightforward. The easiest way to think about this is to treat the NSUserDefaults object like an NSMutableDictionary which persists between App runs because this is exactly what it is. A standard NSMutableDictionary is stored in RAM which is cleared down when the app closes. With NSUserDefaults however, the system automatically writes the dictionary from RAM to a .plist file when the app closes. When the app opens again the system writes the .plist file to RAM again. With this in mind, using NSUserDefaults is very logical. Get a pointer to the dictionary and add some information. When you want to retrieve the information, get another pointer and read the information!

In this example I'll save a series of data types:

  1. // Get a pointer to the NSUserDefaults object
  2. NSUserDefaults * defaults = [NSUserDefaults standardUserDefaults];
  3.  
  4. // Game progress - which levels have been completed
  5. // Note that bools have to be stored as NSNumbers
  6.  
  7. NSMutableDictionary * levelProgress = [NSMutableDictionary dictionaryWithObjectsAndKeys:
  8. [NSNumber numberWithBool:YES], @"island",
  9. [NSNumber numberWithBool:NO], @"forest",
  10. [NSNumber numberWithBool:NO], @"mountain",
  11. [NSNumber numberWithBool:NO], @"desert",
  12. nil];
  13.  
  14. // Save to defaults
  15. [defaults setObject:levelProgress forKey:@"level_progress"];
  16.  
  17. // Storing an array of high scores. Note that integers have to be stored as NSNumbers
  18. NSMutableArray * highScores = [NSMutableArray arrayWithObjects:
  19. [NSNumber numberWithInt:597],
  20. [NSNumber numberWithInt:452],
  21. [NSNumber numberWithInt:365],
  22. [NSNumber numberWithInt:12],
  23. nil];
  24.  
  25. [defaults setObject:highScores forKey:@"high_scores"];
  26.  
  27. // Save a string:
  28.  
  29. NSString * userName = @"Ben";
  30.  
  31. [defaults setObject:userName forKey:@"user_name"];

And that's it, now this data is stored in the system preferences

Retrieving data from NSUserDefaults

Now we just reverse the process to retrieve the data.

  1. // Get a pointer to the NSUserDefaults object
  2. NSUserDefaults * defaults = [NSUserDefaults standardUserDefaults];
  3.  
  4. // Game progress - retrieve which levels have been completed
  5.  
  6. NSMutableDictionary * levelProgress = [defaults objectForKey:@"level_progress"];
  7.  
  8. // Loop over level progress:
  9.  
  10. for(NSString * key in [levelProgress allKeys]) {
  11. NSLog(@"key: %@, complete: %i", key, [levelProgress objectForKey:key]);
  12. }
  13.  
  14. // Outputs:
  15. // key: island, complete: 1
  16. // key: forest, complete: 0
  17. // key: mountain, complete: 0
  18. // key: desert, complete: 0
  19.  
  20. // Retrieve an array of high scores
  21.  
  22. NSMutableArray * highScores = [defaults objectForKey:@"high_scores"];
  23.  
  24. for(NSNumber * score in highScores) {
  25. NSLog(@"Score: %i", [score intValue]);
  26. }
  27.  
  28. // Outputs:
  29. // Score: 597
  30. // Score: 452
  31. // Score: 365
  32. // Score: 12
  33.  
  34. // Retrieve a string:
  35.  
  36. NSString * userName = [defaults objectForKey:@"user_name"];
  37.  
  38. NSLog(userName);
  39.  
  40. // Outputs:
  41. // Ben

So it's as easy as that! Save and load your game state with the knowledge that it will be safely synched with the users computer.! It can get a bit more complex when you start encoding custom objects but I'll cover that in the next tutorial.

Update: Problems updating NSUserDefaults

NSUserDefaults is great but it's not particularly clever. I had a problem recently where I was trying to save a complex set of nested data into the defaults. I had a NSMutableArray containing some dictionaries which in turn contained an NSMutableArray. The first thing I discovered is that when you retrieve the NSMutableArray it's no longer mutable! To get around this you need to do a mutableCopy of the array and then re-insert it into the dictionary. The next problem I found was that even though this was working fine and when I printed my NSUserDefaults my new value was there and I was calling synchronise after making the change; it wasn't actually being saved. Eventually I found the solution. NSUserDefaults doen't notice if you make a change to a nested structure. Because it doesn't notice it doesn't save your new nested structure (even if it appears to be there when you print the NSUserDefaults!). To solve this I just added the root element of the nested structure to NSUserDefaults again.

  1. NSMutableArray * nestedStructure = [[[NSUserDefaults standardUserDefaults] objectForKey: @"nested"] mutableCopy];
  2.  
  3. // Make some change to the structure
  4.  
  5. // Add it the whole thing back into NSUserDefaults
  6. [[NSUserDefaults standardUserDefaults] setObject:nestedStructure forKey:@"nested"];
  7. // Synchronize to ensure it's saved
  8. [[NSUserDefaults standardUserDefaults] synchronize];

Remember, it's safer to re-add your whole structure every time a change is made!

Tweet: 

Comments

If you notice that your preferences are not being saved when the user presses the home button and then quits the app you need to add this line to teh AppDelegate inside the application DidEnterBackground method:

  1. [[NSUserDefaults standardUserDefaults] synchronize];

Thank you Ben!! Wonderful tutorial. Clear and concise.

Add new comment

Filtered HTML

  • Web page addresses and e-mail addresses turn into links automatically.
  • You can enable syntax highlighting of source code with the following tags: <code>, <blockcode>, <c>, <cpp>, <drupal5>, <drupal6>, <java>, <javascript>, <php>, <python>, <ruby>. The supported tag styles are: <foo>, [foo].
  • Allowed HTML tags: <a> <em> <strong> <cite> <blockquote> <code> <ul> <ol> <li> <dl> <dt> <dd>
  • Lines and paragraphs break automatically.

Plain text

  • No HTML tags allowed.
  • Web page addresses and e-mail addresses turn into links automatically.
  • Lines and paragraphs break automatically.