Saving Data with NSUserDefaults and NSCoding

Applications generally have long-lasting consequences: we take a picture, create a presentation, or just unlock a new level. iOS will try to keep your app in memory for a reasonable amount of time, but eventually you need to permanently save something to the disk. There are several ways of doing this, ranging from writing files to using a SQLite database. We’re going to use something in the middle: NSUserDefaults.

NSUserDefaults lets us persist basic objects (strings, numbers, arrays, and hashes) through a simple key-value interface. It handles the serialization mechanics for us, saving you the trouble of constructing a custom file serialization scheme. But it has one more trick up its sleeve: coupled with NSCoding, we can actually save arbitrary objects, not just the primitives classes.

An NSCoding-compliant object implements two specific methods that describe how to save and restore its properties into primitive objects. We can then take this collection of primitive objects and turn it into raw data, which NSUserDefaults understands. That all sounds a bit heady, so let’s try implementing this fancy stuff in our app to get a better idea of what it can do.

First let’s make our User NSCoding compliant. We need to add two new methods: initWithCoder(decoder) and encodeWithCoder(encoder). When we need to serialize and deserialize our object, these methods will be called. The objects they pass as arguments have simple APIs for retrieving and setting values. For our User, they look something like this:

 def​ initWithCoder(decoder)
  self.init
  PROPERTIES.each ​do​ |prop|
  saved_value = decoder.decodeObjectForKey(prop.to_s)
  self.send(​"​​#{​prop​}​​="​, saved_value)
 end
  self
 end
 def​ encodeWithCoder(encoder)
  PROPERTIES.each ​do​ |prop|
  encoder.encodeObject(self.send(prop), ​forKey: ​prop.to_s)
 end
 end

initWithCoder(decoder) is called when we want to deserialize our object out of the decoder instance. Thus, we use decodeObjectForKey(key) on all of our PROPERTIES (see how that keeps making our life easier!).

Conversely, encodeWithCoder(encoder) gets called when we want to save our object and encode its properties with encodeObject:forKey:. The values and keys we use here are exactly those we use in initWithCoder:.

Our User can be encoded and decoded, but now what? We want to save and load the AppDelegate’s @user, which requires us to use the NSUserDefaults. If we’re using primitive types like strings or arrays, saving them directly just works:

 defaults = NSUserDefaults.standardUserDefaults
 defaults[​"some_array"​] = [1,2,3]
 defaults[​"some_number"​] = 4
 
 some_name = defaults[​"some_name"​]

However, putting NSCoding objects such as @user into NSUserDefaults requires NSKeyedArchiver and NSKeyedUnarchiver. These two classes take NSCoding objects and transform them into instances of NSData, which can be safely stored or retrieved from the defaults like normal. NSKeyedArchiver uses the encodeWithCoder: we implemented earlier, while NSKeyedUnarchiver uses initWithCoder:. That’s a lot of NS prefixes, I know. Here’s what an NSCoding serialization looks like:

 my_object = ​# some NSCoding-compliant object
 defaults = NSUserDefaults.standardUserDefaults
 defaults[​"some_object"​] = NSKeyedArchiver.archivedDataWithRootObject(my_object)
 
 my_saved_data = defaults[​"some_object"​]
 my_saved_object = NSKeyedUnarchiver.unarchiveObjectWithData(my_saved_data)

That’s going to get really tedious really fast to repeat everywhere for all our Users, so let’s wrap it in some helper methods.

 USER_KEY = ​"user"
 def​ save
  defaults = NSUserDefaults.standardUserDefaults
  defaults[USER_KEY] = NSKeyedArchiver.archivedDataWithRootObject(self)
 end
 def​ self.load
  defaults = NSUserDefaults.standardUserDefaults
  data = defaults[USER_KEY]
 # protect against nil case
  NSKeyedUnarchiver.unarchiveObjectWithData(data) ​if​ data
 end

Ah, much better. In a production app, our USER_KEY would probably be a function of the user’s id, but since we have only one user in our app, it’s not a big deal. All we have to do now is save and load our object when the app opens and closes.

 @window = UIWindow.alloc.initWithFrame(UIScreen.mainScreen.bounds)
»@user = User.load
»@user ||= User.new(​id: ​​"123"​, ​name: ​​"Clay"​,
»email: ​​"[email protected]"​, ​phone: ​​"555-555-5555"​)
 @user_controller = UserController.alloc.initWithUser(@user)
 def​ applicationDidEnterBackground(application)
  @user.save
 end

In addition to application:didFinishLaunchingWithOptions:, the application delegate can respond to many more application life-cycle methods, similar to UIViewController. Apple recommends we save user data after the application has entered the background (as not to accidentally freeze the interface), so we use that callback to call @user.save.

Why don’t you take our app for a spin, alter some @user properties, and then close our app? In fact, hold down the home button on the simulator and force-quit it with the red icon just to make sure we really got it. Open it back up, and your changes should reappear!

This small app had only one controller, but you can see how KVO and NSUserDefaults scales to more complex objects and relationships. But what about our user interfaces? How can we display hundreds or thousands of models on the screen and keep our app running smoothly? Well, that’s where the incredibly versatile UITableView comes into play in Chapter 5, Showing Data with Table Views.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset