Basic Data Persistence - Beginning iPhone Development: Exploring the iOS SDK, Seventh Edition (2014)

Beginning iPhone Development: Exploring the iOS SDK, Seventh Edition (2014)

Chapter 13. Basic Data Persistence

So far, we’ve focused on the controller and view aspects of the MVC paradigm. Although several of our applications have read data out of the application bundle, none of them has saved data to any form of persistent storage—nonvolatile storage that survives a restart of the computer or device. So far, with the exception of Application Settings (in Chapter 12), every sample application either did not store data or used volatile (i.e., nonpersistent) storage. Every time one of the sample applications launched, it appeared with exactly the same data it had the first time you launched it.

This approach has worked for us up to this point. But in the real world, your applications will need to persist data. When users make changes, they usually like to find those changes when they launch the program again.

A number of different mechanisms are available for persisting data on an iOS device. If you’ve programmed in Cocoa for OS X, you’ve likely used some or all of these techniques.

In this chapter, we’re going to look at four different mechanisms for persisting data to the iOS file system:

· Property lists

· Object archives (or archiving)

· SQLite3 (iOS’s embedded relational database)

· Core Data (Apple’s provided persistence tool)

We will write example applications that use all four approaches.

Note Property lists, object archives, SQLite3, and Core Data are not the only ways you can persist data on iOS; they are just the most common and easiest. You always have the option of using traditional C I/O calls like fopen() to read and write data. You can also use Cocoa’s low-level file-management tools. In almost every case, doing so will result in a lot more coding effort and is rarely necessary, but those tools are there if you want them.

Your Application’s Sandbox

All four of this chapter’s data-persistence mechanisms share an important common element: your application’s /Documents folder. Every application gets its own /Documents folder, and applications are allowed to read and write from their own /Documents directory.

To give you some context, let’s take a look at how applications are organized in iOS by examining the folder layout used by the iPhone simulator. To see this, you’ll need to look inside the Library directory contained in your home directory. On OS X 10.6 and earlier, this was no problem; however, starting with OS X 10.7, Apple decided to make the Library folder hidden by default, so there’s a small extra hoop to jump through. Open a Finder window and navigate to your home directory. If you can see your Library folder, that’s great. If not, hold down the Alt key and selectGo image Library. The Library option is hidden unless you hold down the Alt key.

Within the Library folder, drill down into Developer/CoreSimulator/Devices/. Within that directory, you’ll see one subdirectory for each simulator in your current Xcode installation. The subdirectory names are globally unique identifiers (GUIDs) that are generated automatically by Xcode, so it’s impossible to know just by looking at them which directory corresponds to which simulator. To find out, look for a file called device.plist in any of the simulator directories and open it. You’ll find a key that maps to the simulated device’s name. Figure 13-1 shows the device.plist file for the iPad 2 simulator.

image

Figure 13-1. Using the device.plist file to map a directory to a simulator

Choose a device and drill down into its data directory until you reach the subdirectory data/Containers/Data/Application. Here again you’ll see subdirectories with names that are GUIDs. In this case, each one of them represents either a preinstalled application or an application that you have run on that simulator. Select one of the directories and open it. You’ll see something like Figure 13-2.

image

Figure 13-2. The sandbox for an application on the simulator

Although this listing represents the simulator, the file structure is similar to what’s on the actual device. To see the sandbox for an application on a device, plug it onto your Mac and open the Xcode Devices window (Window image Devices). You should see your device in the window sidebar. Select it and then choose an application from the Installed Apps table. Below the table, there’s an icon that looks like a gear. Click it and select Show Container from the pop-up to see the contents of the application’s sandbox. You can also download everything in the sandbox to your Mac.Figure 13-3 shows the application sandbox for the Bridge Control application that we created in Chapter 12.

image

Figure 13-3. The sandbox for an application on a real device

Every application sandbox contains these three directories:

· Documents: Your application can store data in Documents. If you enable iTunes file sharing for your application, the user can see the contents of this directory (and any subdirectories that your application creates) in iTunes and can also upload files to it.

Tip To enable file sharing for your application, open its Info.plist file and add the key Application supports iTunes file sharing with the value YES.

· Library: This is another place that your application can use to store its data. Use it for files that you do not want to share with the user. You can create your own subdirectories if required. As you can see in Figure 13-3, the system creates subdirectories called Cache andPreferences. The latter contains the .plist file that stores the application’s preferences, set using the NSUserDefaults class, which we discussed in Chapter 12.

· tmp: The tmp directory offers a place where your application can store temporary files. Files written into tmp will not be backed up by iTunes when your iOS device syncs; but to avoid filling up the file system, your application does need to take responsibility for deleting the files in tmp once they are no longer needed.

Getting the Documents and Library Directories

Since our application is in a folder with a seemingly random name, how do we retrieve the full path to the Documents directory so that we can read and write our files? It’s actually quite easy. The C function NSSearchPathForDirectoriesInDomain() will locate various directories for you. This is a Foundation function, so it is shared with Cocoa for OS X. Many of its available options are designed for OS X and won’t return any values on iOS, either because those locations don’t exist on iOS (such as the Downloads folder) or because your application doesn’t have rights to access the location due to iOS’s sandboxing mechanism.

Here’s some code to retrieve the path to the Documents directory:

NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,
NSUserDomainMask, YES);
NSString *documentsDirectory = paths[0];

The constant NSDocumentDirectory says we are looking for the path to the Documents directory. The second constant, NSUserDomainMask, indicates that we want to restrict our search to our application’s sandbox. In OS X, this same constant is used to indicate that we want the function to look in the user’s home directory, which explains its somewhat odd name.

Though an array of matching paths is returned, we can count on our Documents directory residing at index zero in the array. Why? We know that only one directory meets the criteria we’ve specified, since each application has only one Documents directory.

We can create a file name by appending another string onto the end of the path we just retrieved. We’ll use an NSString method called stringByAppendingPathComponent: that was designed for just that purpose:

NSString *filename = [documentsDirectory
stringByAppendingPathComponent:@"theFile.txt"];

After this call, filename would contain the full path to a file called theFile.txt in our application’s Documents directory, and we can use filename to create, read, and write from that file.

You can use the same C function with first argument NSLibraryDirectory to locate the Library directory:

NSArray *paths = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory,
NSUserDomainMask, YES);
NSString *libraryDirectory = paths[0];

Getting the tmp Directory

Getting a reference to your application’s temporary directory is even easier than getting a reference to the Documents directory. The Foundation function called NSTemporaryDirectory() will return a string containing the full path to your application’s temporary directory. To create a file name for a file that will be stored in the temporary directory, first find the temporary directory:

NSString *tempPath = NSTemporaryDirectory();

Next, create a path to a file in that directory by appending a file name to that path, like this:

NSString *tempFile = [tempPath
stringByAppendingPathComponent:@"tempFile.txt"];

File-Saving Strategies

All four approaches we’re going to look at in this chapter use the iOS file system. In the case of SQLite3, you’ll create a single SQLite3 database file and let SQLite3 worry about storing and retrieving your data. In its simplest form, Core Data takes care of all the file system management for you. With the other two persistence mechanisms—property lists and archiving—you need to put some thought into whether you are going to store your data in a single file or in multiple files.

Single-File Persistence

Using a single file for data storage is the easiest approach; and with many applications, it is a perfectly acceptable one. You start by creating a root object, usually an NSArray or NSDictionary (your root object can also be based on a custom class when using archiving). Next, you populate your root object with all the program data that needs to be persisted. Whenever you need to save, your code rewrites the entire contents of that root object to a single file. When your application launches, it reads the entire contents of that file into memory. When it quits, it writes out the entire contents. This is the approach we’ll use in this chapter.

The downside of using a single file is that you need to load all of your application’s data into memory, and you must write all of it to the file system for even the smallest changes. But if your application isn’t likely to manage more than a few megabytes of data, this approach is probably fine, and its simplicity will certainly make your life easier.

Multiple-File Persistence

Using multiple files for persistence is an alternative approach. For example, an e-mail application might store each e-mail message in its own file.

There are obvious advantages to this method. It allows the application to load only data that the user has requested (another form of lazy loading); and when the user makes a change, only the files that changed need to be saved. This method also gives you the opportunity to free up memory when you receive a low-memory notification. Any memory that is being used to store data that the user is not currently viewing can be flushed and then simply reloaded from the file system the next time it’s needed.

The downside of multiple-file persistence is that it adds a fair amount of complexity to your application. For now, we’ll stick with single-file persistence.

Next, we’ll get into the specifics of each of our persistence methods: property lists, object archives, SQLite3, and Core Data. We’ll explore each of these in turn and build an application that uses each mechanism to save some data to the device’s file system. We’ll start with property lists.

Using Property Lists

Several of our sample applications have used property lists, most recently when we used a property list to specify our application settings and preferences in Chapter 12. Property lists are convenient. They can be edited manually using Xcode or the Property List Editor application. Also, bothNSDictionary and NSArray instances can be written to and created from property lists, as long as the dictionary or array contains only specific serializable objects.

Property List Serialization

A serialized object is one that has been converted into a stream of bytes so that it can be stored in a file or transferred over a network. Although any object can be made serializable, only certain objects can be placed into a collection class, such as an NSDictionary or NSArray, and then stored to a property list using the collection class’s writeToFile:atomically: or writeToURL:atomically: methods. The following Foundation classes can be serialized this way:

· NSArray

· NSMutableArray

· NSDictionary

· NSMutableDictionary

· NSData

· NSMutableData

· NSString

· NSMutableString

· NSNumber

· NSDate

If you can build your data model from just these objects, you can use property lists to save and load your data.

If you’re going to use property lists to persist your application data, you’ll use either an NSArray or an NSDictionary to hold the data that needs to be persisted. Assuming that all the objects that you put into the NSArray or NSDictionary are serializable objects from the preceding list, you can write out a property list by calling the writeToFile:atomically: method on the dictionary or array instance, like so:

[myArray writeToFile:@"/some/file/location/output.plist" atomically:YES];

Note In case you were wondering, the atomically parameter tells the method to write the data to an auxiliary file, not to the specified location. Once it has successfully written the file, it will then copy that auxiliary file to the location specified by the first parameter. This is a safer way to write a file, because if the application crashes during the save, the existing file (if there was one) will not be corrupted. It adds a bit of overhead; but in most situations, it’s worth the cost.

One problem with the property list approach is that custom objects cannot be serialized into property lists. You also can’t use other classes from Cocoa Touch that aren’t specified in the list of serializable object types, which means that classes like NSURL, UIImage, and UIColor cannot be used directly.

Apart from the serialization issue, keeping all your model data in the form of property lists means that you can’t easily create derived or calculated properties (such as a property that is the sum of two other properties), and some of your code that really should be contained in model classes must be moved to your controller classes. Again, these restrictions are OK for simple data models and simple applications. Most of the time, however, your application will be much easier to maintain if you create dedicated model classes.

Simple property lists can still be useful in complex applications. They are a great way to include static data in your application. For example, when your application has a picker, often the best way to include the list of items for it is to create a .plist file and place that file in your project’sResources folder, which will cause it to be compiled into your application.

Let’s a build a simple application that uses property lists to store its data.

The First Version of the Persistence Application

We’re going to build a program that lets you enter data into four text fields, saves those fields to a .plist file when the application quits, and then reloads the data back from that .plist file the next time the application launches (see Figure 13-4).

image

Figure 13-4. The Persistence application

Note In this chapter’s applications, we won’t be taking the time to set up all the user interface niceties that we have added in previous examples. Tapping the Return key, for example, will neither dismiss the keyboard nor take you to the next field. If you want to add such polish to the application, doing so would be good practice, so we encourage you to do that on your own.

Creating the Persistence Project

In Xcode, create a new project using the Single View Application template and name it Persistence. This project contains all the files that we’ll need to build our application, so we can dive right in.

Before we build the view with the four text fields, let’s create the outlets we need. In the Project Navigator, single-click the ViewController.m file and make the following changes:

#import "ViewController.h"

@interface ViewController ()

@property (strong, nonatomic) IBOutletCollection(UITextField) NSArray *lineFields;

@end

Now select Main.storyboard to edit the GUI.

Designing the Persistence Application View

Once Xcode switches over to Interface Builder mode, you’ll see the View Controller scene in the editing pane. Expand the View Controller icon and change the name of the View item to Main View. Drag a Text Field from the library and place it against the top and right blue guidelines. Bring up the Attributes Inspector. Make sure the box labeled Clear when editing begins is unchecked.

Now drag a Label to the window and place it to the left of the text field using the left blue guideline, and then use the horizontal blue guideline to line up the label’s vertical center with that of the text field. Double-click the label and change it to say Line 1:. Finally, resize the text field using the left resize handle to bring it close to the label. Use Figure 13-5 as a guide.

image

Figure 13-5. Designing the Persistence application’s view

Next, select the label and text field, hold down the Option key, and drag down to make a copy below the first set. Use the blue guidelines to guide your placement. Now select both labels and both text fields, hold down the Option key, and drag down again. You should have four labels next to four text fields. Double-click each of the remaining labels and change their names to Line 2:, Line 3:, and Line 4:. Again, compare your results with Figure 13-5.

Once you have all four text fields and labels placed, Control-drag from the View Controller icon to each of the four text fields. Connect them all to the lineFields outlet collection, making sure to connect them in order from top to bottom. Save the changes you made to Main.storyboard.

Now let’s add the Auto Layout constraints to make sure that the design works the same way on all devices. Starting by Control-dragging from the Line 1 label to the text field to its right, and then release the mouse. Hold down the Shift key and select Horizontal Spacing and Baseline, and then click outside the pop-up. Do the same for the other three labels and text fields.

Next, we’ll fix the positions of the text fields. In the Document Outline, Control-drag from the top text field to Main View, release the mouse, hold down the Shift key and select Trailing Space to Container Margin and Top Space to Top Layout Guide, and then click outside the pop-up. Do the same for the other three text fields.

We need to fix the widths of the labels so that they don’t resize if the user types more text than will fit in any of the text fields. Select the top label and click the Pin button below the storyboard editor. In the pop-up, select the Width check box and press Add 1 Constraint. Do the same for all of the labels.

Finally, back in the Document Outline, Control-drag from the Line 1 label to Main View, release the mouse, and select Leading Space to Container Margin. Do the same for all of the labels and that’s it—all the required Auto Layout constraints have been set. Build and run the application and compare the result with Figure 13-5.

Editing the Persistence Classes

In the Project Navigator, select ViewController.m and add the following code to the class’s @implementation section:

@implementation ViewController

- (NSString *)dataFilePath
{
NSArray *paths = NSSearchPathForDirectoriesInDomains(
NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = [paths objectAtIndex:0];
return [documentsDirectory stringByAppendingPathComponent:@"data.plist"];
}

The dataFilePath method returns the full pathname of our data file by finding the Documents directory and appending our file name to it. This method will be called from any code that needs to load or save data.

Find the viewDidLoad method and add the following code to it, as well as a new method for receiving notifications named applicationWillResignActive: just below it, like this:

- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
NSString *filePath = [self dataFilePath];
if ([[NSFileManager defaultManager] fileExistsAtPath:filePath]) {
NSArray *array = [[NSArray alloc] initWithContentsOfFile:filePath];
for (int i = 0; i < 4; i++) {
UITextField *theField = self.lineFields[i];
theField.text = array[i];
}
}

UIApplication *app = [UIApplication sharedApplication];
[[NSNotificationCenter defaultCenter]
addObserver:self
selector:@selector(applicationWillResignActive:)
name:UIApplicationWillResignActiveNotification
object:app];
}

- (void)applicationWillResignActive:(NSNotification *)notification {
NSString *filePath = [self dataFilePath];
NSArray *array = [self.lineFields valueForKey:@"text"];
[array writeToFile:filePath atomically:YES];
}

In the viewDidLoad method, we do a few more things. First, we use the NSFileManager class to check whether a data file already exists. If there isn’t one, we don’t want to bother trying to load it. If the file does exist, we instantiate an array with the contents of that file, and then copy the objects from that array to our four text fields. Because arrays are ordered lists, we copy them in the same order as we save them (the code for which you haven’t yet seen), so that we are always sure to get the correct values in the correct fields:

NSString *filePath = [self dataFilePath];
if ([[NSFileManager defaultManager] fileExistsAtPath:filePath]) {
NSArray *array = [[NSArray alloc] initWithContentsOfFile:filePath];
for (int i = 0; i < 4; i++) {
UITextField *theField = self.lineFields[i];
theField.text = array[i];
}
}

After we load the data from the property list, we get a reference to our application instance and use that to subscribe to UIApplicationWillResignActiveNotification, using the default NSNotificationCenter instance and a method calledaddObserver:selector:name:object:. We pass self as the first parameter, specifying that our ViewController instance is the observer that should be notified. For the second parameter, we pass a selector to the applicationWillResignActive: method, telling the notification center to call that method when the notification is posted. The third parameter, UIApplicationWillResignActiveNotification, is the name of the notification that we’re interested in receiving. This is a string constant defined by the UIApplication class. The final parameter, app, is the object we’re interested in getting the notification from:

UIApplication *app = [UIApplication sharedApplication];
[[NSNotificationCenter defaultCenter]
addObserver:self
selector:@selector(applicationWillResignActive:)
name:UIApplicationWillResignActiveNotification
object:app];

The final new method is called applicationWillResignActive:. Notice that it takes a pointer to an NSNotification as an argument. You probably recognize this pattern from Chapter 12. applicationWillResignActive: is a notification method, and all notifications take a single NSNotification instance as their argument.

Our application needs to save its data before it is terminated or sent to the background, so we are interested in the notification called UIApplicationWillResignActiveNotification. This notification is posted whenever an app is no longer the one with which the user is interacting. This happens when the user taps the Home button, as well as when the application is pushed to the background by some other event, such as an incoming phone call. Earlier, in the viewDidLoad method, we used the notification center to subscribe to that particular notification. This method is called when that notification happens:

- (void)applicationWillResignActive:(NSNotification *)notification {
NSString *filePath = [self dataFilePath];
NSArray *array = [self.lineFields valueForKey:@"text"];
[array writeToFile:filePath atomically:YES];
}

This method is pretty short, but really does a lot with just a few method calls. We construct an array of strings by calling the text method on each of the text fields in our lineFields array. To accomplish this, we use a clever shortcut: instead of explicitly iterating through our array of text fields, asking each for its text value, and adding that value to a new array, we simply call valueForKey: on our array, passing @"text" as a parameter. The NSArray implementation of valueForKey: does the iteration for us, asks each UITextField instance it contains for itstext value, and returns a new array containing all the values. After that, we write the contents of that array out to our .plist file. That’s all there is to saving our data using property lists.

That wasn’t too bad, was it? When our main view is finished loading, we look for a .plist file. If it exists, we copy data from it into our text fields. Next, we register to be notified when the application becomes inactive (either by being quit or pushed to the background). When that happens, we gather the values from our four text fields, stick them in a mutable array, and write that mutable array to a property list.

Why don’t you compile and run the application? It should build and then launch in the simulator. Once it comes up, you should be able to type into any of the four text fields. When you’ve typed something in them, press the Home button (the circular button with the rounded square in it at the bottom of the simulator window). It’s very important that you press the Home button. If you just exit the simulator, that’s the equivalent of forcibly quitting your application. In that case, the view controller will never receive the notification that the application is going inactive, and your data will not be saved. After pressing the Home button, you may quit the simulator, or stop the app from Xcode and run it again. Your text will be restored the next time the app starts.

Note It’s important to understand that pressing the Home button doesn’t typically quit the app—at least not at first. The app is put into a background state, ready to be instantly reactivated in case the user switches back to it. We’ll dig into the details of these states and their implications for running and quitting apps in Chapter 15. In the meantime, if you want to verify that the data really was saved, you can quit the iOS simulator entirely and then restart your app from Xcode. Quitting the simulator is basically the equivalent of rebooting an iPhone. The next time your app starts, it will give the user a fresh relaunch experience.

Property list serialization is pretty cool and easy to use. However, it’s a little limiting, since only a small selection of objects can be stored in property lists. Let’s look at a somewhat more robust approach.

Archiving Model Objects

In the Cocoa world, the term archiving refers to another form of serialization, but it’s a more generic type that any object can implement. Any model object specifically written to hold data should support archiving. The technique of archiving model objects lets you easily write complex objects to a file and then read them back in.

As long as every property you implement in your class is either a scalar (e.g., int or float) or an instance of a class that conforms to the NSCoding protocol, you can archive your objects completely. Since most Foundation and Cocoa Touch classes capable of storing data do conform toNSCoding (though there are a few noteworthy exceptions, such as UIImage), archiving is relatively easy to implement for most classes.

Although not strictly required to make archiving work, another protocol should be implemented along with NSCoding: the NSCopying protocol, which allows your object to be copied. Being able to copy an object gives you a lot more flexibility when using data model objects.

Conforming to NSCoding

The NSCoding protocol declares two methods, which are both required. One encodes your object into an archive; the other one creates a new object by decoding an archive. Both methods are passed an instance of NSCoder, which you work with in very much the same way asNSUserDefaults, introduced in the previous chapter. You can encode and decode both objects and native datatypes like int and float values using key-value coding.

A method to encode an object might look like this:

- (void)encodeWithCoder:(NSCoder *)encoder {
[encoder encodeObject:foo forKey:kFooKey];
[encoder encodeObject:bar forKey:kBarKey];
[encoder encodeInt:someInt forKey:kSomeIntKey];
[encoder encodeFloat:someFloat forKey:kSomeFloatKey]
}

To support archiving in our object, we need to encode each of our instance variables into encoder using the appropriate encoding method. If you are subclassing a class that also conforms to NSCoding, you need to make sure you call encodeWithCoder: on your superclass to ensure that the superclass encodes its data. Therefore, your method would look like this instead:

- (void)encodeWithCoder:(NSCoder *)encoder {
[super encodeWithCoder:encoder]; // Let superclass encode its state
[encoder encodeObject:foo forKey:kFooKey];
[encoder encodeObject:bar forKey:kBarKey];
[encoder encodeInt:someInt forKey:kSomeIntKey];
[encoder encodeFloat:someFloat forKey:kSomeFloatKey]
}

We also need to implement a method that initializes an object from an NSCoder, allowing us to restore an object that was previously archived. Implementing the initWithCoder: method is slightly more complex than implementing encodeWithCoder:. If you are subclassingNSObject directly or subclassing some other class that doesn’t conform to NSCoding, your method would look something like the following:

- (id)initWithCoder:(NSCoder *)decoder {
if (self = [super init]) {
foo = [decoder decodeObjectForKey:kFooKey];
bar = [decoder decodeObjectForKey:kBarKey];
someInt = [decoder decodeIntForKey:kSomeIntKey];
someFloat = [decoder decodeFloatForKey:kAgeKey];
}
return self;
}

The method initializes an object instance using [super init]. If that’s successful, it sets its properties by decoding values from the passed-in instance of NSCoder. When implementing NSCoding for a class with a superclass that also conforms to NSCoding, the initWithCoder:method needs to look slightly different. Instead of calling init on super, it needs to call initWithCoder:, like so:

- (id)initWithCoder:(NSCoder *)decoder {
if (self = [super initWithCoder:decoder]) {
foo = [decoder decodeObjectForKey:kFooKey];
bar = [decoder decodeObjectForKey:kBarKey];
someInt = [decoder decodeIntForKey:kSomeIntKey];
someFloat = [decoder decodeFloatForKey:kAgeKey];
}
return self;
}

And that’s basically it. As long as you implement these two methods to encode and decode all your object’s properties, your object is archivable and can be written to and read from archives.

Implementing NSCopying

As mentioned earlier, conforming to NSCopying is a very good idea for any data model objects. NSCopying has one method, called copyWithZone:, which allows objects to be copied. Implementing NSCopying is similar to implementing initWithCoder:. You just need to create a new instance of the same class, and then set all of that new instance’s properties to the same values as this object’s properties. Here’s what a copyWithZone: method might look like:

- (id)copyWithZone:(NSZone *)zone {
MyClass *copy = [[[self class] allocWithZone:zone] init];
copy.foo = [self.foo copyWithZone:zone];
copy.bar = [self.bar copyWithZone:zone];
copy.someInt = self.someInt;
copy.someFloat = self.someFloat;
return copy;
}

Note Don’t worry too much about the NSZone parameter. This pointer is to a struct that is used by the system to manage memory. Only in rare circumstances did developers ever need to worry about zones or create their own, and nowadays, it’s almost unheard of to have multiple zones. Calling copy on an object is the same as calling copyWithZone: using the default zone, which is always what you want. In fact, on the modern iOS, zones are completely ignored. The fact that NSCopying uses zones at all is a historical oddity for the sake of backward compatibility.

Archiving and Unarchiving Data Objects

Creating an archive from an object (or objects) that conforms to NSCoding is relatively easy. First, we create an instance of NSMutableData to hold the encoded data, and then we create an NSKeyedArchiver instance to archive objects into that NSMutableData instance:

NSMutableData *data = [[NSMutableData alloc] init];
NSKeyedArchiver *archiver = [[NSKeyedArchiver alloc]
initForWritingWithMutableData:data];

After creating both of those, we then use key-value coding to archive any objects we wish to include in the archive, like this:

[archiver encodeObject:myObject forKey:@"keyValueString"];

Once we’ve encoded all the objects we want to include, we just tell the archiver we’re finished, and then we write the NSMutableData instance to the file system:

[archiver finishEncoding];
BOOL success = [data writeToFile:@"/path/to/archive" atomically:YES];

If anything went wrong while writing the file, success will be set to NO. If success is YES, the data was successfully written to the specified file. Any objects created from this archive will be exact copies of the objects that were last written into the file.

There is a quicker way to achieve the same thing, using the NSKeyedArchiver archiveDataWithRootObject: method, which allocates an NSData object and encodes the object into it in a single step, then returns the NSData object:

NSData *data = [NSKeyedArchiver archivedDataWithRootObject:object];
BOOL success = [data writeToFile:@"/path/to/archive" atomically:YES];

You can also go straight from the object to the file using the archiveRootObject:toFile: method:

BOOL success = [NSKeyedArchiver archiveRootObject:object
toFile:@"/path/to/archive"];

To reconstitute objects from the archive, we go through a similar process. We create an NSData instance from the archive file and create an NSKeyedUnarchiver to decode the data:

NSData *data = [[NSData alloc] initWithContentsOfFile:@"/path/to/archive"];
NSKeyedUnarchiver *unarchiver = [[NSKeyedUnarchiver alloc]
initForReadingWithData:data];

After that, we read our objects from the unarchiver using the same key that we used to archive the object:

self.object = [unarchiver decodeObjectForKey:@"keyValueString"];

Finally, we tell the archiver we are finished:

[unarchiver finishDecoding];

As with the archiving step, there are convenience methods that let you unarchive directly from an NSData object or from a file without allocating an NSKeyedUnarchiver instance.

If you’re feeling a little overwhelmed by archiving, don’t worry. It’s actually fairly straightforward. We’re going to retrofit our Persistence application to use archiving, so you’ll get to see it in action. Once you’ve done it a few times, archiving will become second nature, as all you’re really doing is storing and retrieving your object’s properties using key-value coding.

The Archiving Application

Let’s redo the Persistence application, so it uses archiving instead of property lists. We’re going to be making some fairly significant changes to the Persistence source code, so you should make a copy of your entire project folder before continuing.

Implementing the FourLines Class

Once you’re ready to proceed and have a copy of your Persistence project open in Xcode, press imageN or select File image New image File…. When the new file assistant comes up, from the iOS section, select Cocoa Touch Class and click Next. On the next screen, name the class FourLines and select NSObject in the Subclass of control. Click Next again. Now choose the Persistence folder to save the files, and then click Create. This class is going to be our data model. It will hold the data that we’re currently storing in a dictionary in the property list application.

Single-click FourLines.h and make the following changes:

#import <Foundation/Foundation.h>

@interface FourLines : NSObject <NSCoding, NSCopying>

@property (copy, nonatomic) NSArray *lines;

@end

This is a very straightforward data model class with an array property of four strings. Notice that we’ve conformed the class to the NSCoding and NSCopying protocols. Now switch over to FourLines.m and add the following code:

#import "FourLines.h"

static NSString * const kLinesKey = @"kLinesKey";

@implementation FourLines

#pragma mark - Coding

- (id)initWithCoder:(NSCoder *)aDecoder {
self = [super init];
if (self) {
self.lines = [aDecoder decodeObjectForKey:kLinesKey];
}
return self;
}

- (void)encodeWithCoder:(NSCoder *)aCoder; {
[aCoder encodeObject:self.lines forKey:kLinesKey];
}

#pragma mark - Copying

- (id)copyWithZone:(NSZone *)zone; {
FourLines *copy = [[[self class] allocWithZone:zone] init];
NSMutableArray *linesCopy = [NSMutableArray array];
for (id line in self.lines) {
[linesCopy addObject:[line copyWithZone:zone]];
}
copy.lines = linesCopy;
return copy;
}

@end

We just implemented all the methods necessary to conform to NSCoding and NSCopying. We encoded the lines property in encodeWithCoder: and decoded it using the same key value in initWithCoder:. In copyWithZone:, we created a new FourLines object and copied the array of strings to it. See? It’s not hard at all; just make sure you did not forget to change anything if you did a lot of copying and pasting.

Implementing the ViewController Class

Now that we have an archivable data object, let’s use it to persist our application data. Select ViewController.m and make the following changes:

#import "ViewController.h"
#import "FourLines.h"

static NSString * const kRootKey = @"kRootKey";

@interface ViewController ()

@property (strong, nonatomic) IBOutletCollection(UITextField) NSArray *lineFields;

@end

@implementation ViewController

- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
NSString *filePath = [self dataFilePath];
if ([[NSFileManager defaultManager] fileExistsAtPath:filePath]) {
NSArray *array = [[NSArray alloc] initWithContentsOfFile:filePath];
for (int i = 0; i < 4; i++) {
UITextField *theField = self.lineFields[i];
theField.text = array[i];
}
NSData *data = [[NSMutableData alloc]
initWithContentsOfFile:filePath];
NSKeyedUnarchiver *unarchiver = [[NSKeyedUnarchiver alloc]
initForReadingWithData:data];
FourLines *fourLines = [unarchiver decodeObjectForKey:kRootKey];
[unarchiver finishDecoding];

for (int i = 0; i < 4; i++) {
UITextField *theField = self.lineFields[i];
theField.text = fourLines.lines[i];
}
}

UIApplication *app = [UIApplication sharedApplication];
[[NSNotificationCenter defaultCenter]
addObserver:self
selector:@selector(applicationWillResignActive:)
name:UIApplicationWillResignActiveNotification
object:app];
}

- (void)applicationWillResignActive:(NSNotification *)notification
{
NSString *filePath = [self dataFilePath];
NSArray *array = [self.lineFields valueForKey:@"text"];
[array writeToFile:filePath atomically:YES];

FourLines *fourLines = [[FourLines alloc] init];
fourLines.lines = [self.lineFields valueForKey:@"text"];
NSMutableData *data = [[NSMutableData alloc] init];
NSKeyedArchiver *archiver = [[NSKeyedArchiver alloc]
initForWritingWithMutableData:data];
[archiver encodeObject:fourLines forKey:kRootKey];
[archiver finishEncoding];
[data writeToFile:filePath atomically:YES];
}

- (NSString *)dataFilePath
{
NSArray *paths = NSSearchPathForDirectoriesInDomains(
NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = [paths objectAtIndex:0];
return [documentsDirectory stringByAppendingPathComponent:@"data.plist"];
return [documentsDirectory stringByAppendingPathComponent:@"data.archive"];
}

@end

Save your changes and take this version of Persistence for a spin.

Not very much has changed, really. We started off by specifying a new file name so that our program doesn’t try to load the old property list as an archive. We also defined a new constant that will be the key value we use to encode and decode our object. Next, we redefined the loading and saving by using FourLines to hold the data and using its NSCoding methods to do the actual loading and saving. The GUI is identical to the previous version.

This new version takes several more lines of code to implement than property list serialization, so you might be wondering if there really is an advantage to using archiving over just serializing property lists. For this application, the answer is simple: no, there really isn’t any advantage. But imagine we had an array of archivable objects, such as the FourLines class that we just built, we could archive the entire array by archiving the array instance itself. Collection classes like NSArray, when archived, archive all of the objects they contain. As long as every object you put into an array or dictionary conforms to NSCoding, you can archive the array or dictionary and restore it so that all the objects that were in it when you archived it will be in the restored array or dictionary. The same is not true of property link persistence, which only works for a small set of Foundation object types—you cannot use it to persist custom classes without writing additional code to convert instances of those classes to and from an NSDictionary, with one key for each object property.

In other words, the NSCoding approach scales beautifully (in terms of code size, at least). No matter how many objects you add, the work to write those objects to disk (assuming you’re using single-file persistence) is exactly the same. With property lists, the amount of work increases with every object you add.

Using iOS’s Embedded SQLite3

The third persistence option we’re going to discuss is using iOS’s embedded SQL database, called SQLite3. SQLite3 is very efficient at storing and retrieving large amounts of data. It’s also capable of doing complex aggregations on your data, with much faster results than you would get doing the same thing using objects.

Consider a couple scenarios. What if your application needs to calculate the sum of a particular field across all the objects in your application? Or, what if you need the sum from just the objects that meet certain criteria? SQLite3 allows you to get this information without loading every object into memory. Getting aggregations from SQLite3 is several orders of magnitude faster than loading all the objects into memory and summing their values. Being a full-fledged embedded database, SQLite3 contains tools to make it even faster by, for example, creating table indexes that can speed up your queries.

Note There are several schools of thought about the pronunciation of “SQL” and “SQLite.” Most official documentation says to pronounce “SQL” as “Ess-Queue-Ell” and “SQLite” as “Ess-Queue-Ell-Light.” Many people pronounce them, respectively, as “Sequel” and “Sequel Light.” A small cadre of hardened rebels prefer “Squeal” and “Squeal Light.” Pick whatever works best for you (and be prepared to be mocked and shunned by the infidels if you choose to join the “Squeal” movement).

SQLite3 uses the Structured Query Language (SQL), the standard language used to interact with relational databases. Whole books have been written on the syntax of SQL (hundreds of them, in fact), as well as on SQLite itself. So if you don’t already know SQL and you want to use SQLite3 in your application, you have a little work ahead of you. We’ll show you how to set up and interact with the SQLite database from your iOS applications, and we’ll also show you some of the basics of the syntax in this chapter. But to really make the most of SQLite3, you’ll need to do some additional research and exploration. A couple of good starting points are “An Introduction to the SQLite3 C/C++ Interface” (www.sqlite.org/cintro.html) and “SQL As Understood by SQLite” (www.sqlite.org/lang.html).

Relational databases (including SQLite3) and object-oriented programming languages use fundamentally different approaches to storing and organizing data. The approaches are different enough that numerous techniques and many libraries and tools for converting between the two have been developed. These different techniques are collectively called object-relational mapping (ORM). There are currently several ORM tools available for Cocoa Touch. In fact, we’ll look at one ORM solution provided by Apple, called Core Data, later in the chapter.

But before we do that, we’re going to focus on the SQLite3 basics, including setting it up, creating a table to hold your data, and using the database in an application. Obviously, in the real world, an application as simple as the one we’re working on wouldn’t warrant the investment in SQLite3. But this application’s simplicity is exactly what makes it a good learning example.

Creating or Opening the Database

Before you can use SQLite3, you must open the database. The function that’s used to do that, sqlite3_open(), will open an existing database; or, if none exists at the specified location, the function will create a new one. Here’s what the code to open a database might look like:

sqlite3 *database;
int result = sqlite3_open("/path/to/database/file", &database);

If result is equal to the constant SQLITE_OK, then the database was successfully opened. Note that the path to the database file must be passed in as a C string, not as an NSString. SQLite3 was written in portable C, not Objective-C, and it has no idea what an NSString is. Fortunately, there is an NSString method that generates a C string from an NSString instance:

const char *stringPath = [pathString UTF8String];

When you’re finished with an SQLite3 database, close it:

sqlite3_close(database);

Databases store all their data in tables. You can create a new table by crafting an SQL CREATE statement and passing it in to an open database using the function sqlite3_exec, like so:

char *errorMsg;
const char *createSQL = "CREATE TABLE IF NOT EXISTS PEOPLE"
"(ID INTEGER PRIMARY KEY AUTOINCREMENT, FIELD_DATA TEXT)";
int result = sqlite3_exec(database, createSQL, NULL, NULL, &errorMsg);

Tip If two inline strings are separated by nothing but white space, including line breaks, they are concatenated into a single string.

As before, you need to verify that result is equal to SQLITE_OK to make sure your command ran successfully. If it didn’t, errorMsg will contain a description of the problem that occurred.

The function sqlite3_exec is used to run any command against SQLite3 that doesn’t return data, including updates, inserts, and deletes. Retrieving data from the database is a little more involved. You first need to prepare the statement by feeding it your SQL SELECT command:

NSString *query = @"SELECT ID, FIELD_DATA FROM FIELDS ORDER BY ROW";
sqlite3_stmt *statement;
int result = sqlite3_prepare_v2(database, [query UTF8String],
-1, &statement, nil);

Note All of the SQLite3 functions that take strings require an old-fashioned C string. In the example, we created and passed a C string. Specifically, we created an NSString and derived a C string by using one of NSString’s methods called UTF8String. Either method is acceptable. If you need to do manipulation on the string, using NSString or NSMutableString will be easier; however, converting from NSString to a C string incurs a bit of extra overhead.

If result equals SQLITE_OK, your statement was successfully prepared, and you can start stepping through the result set. Here is an example of stepping through a result set and retrieving an int and an NSString from the database:

while (sqlite3_step(statement) == SQLITE_ROW) {
int rowNum = sqlite3_column_int(statement, 0);
char *rowData = (char *)sqlite3_column_text(statement, 1);
NSString *fieldValue = [[NSString alloc] initWithUTF8String:rowData];
// Do something with the data here
}
sqlite3_finalize(statement);

Using Bind Variables

Although it’s possible to construct SQL strings to insert values, it is common practice to use something called bind variables for this purpose. Handling strings correctly—making sure they don’t have invalid characters and that quotes are inserted properly—can be quite a chore. With bind variables, those issues are taken care of for us.

To insert a value using a bind variable, you create your SQL statement as normal, but put a question mark (?) into the SQL string. Each question mark represents one variable that must be bound before the statement can be executed. Next, you prepare the SQL statement, bind a value to each of the variables, and execute the command.

Here’s an example that prepares an SQL statement with two bind variables, binds an int to the first variable and a string to the second variable, and then executes and finalizes the statement:

char *sql = "insert into foo values (?, ?);";
sqlite3_stmt *stmt;
if (sqlite3_prepare_v2(database, sql, -1, &stmt, nil) == SQLITE_OK) {
sqlite3_bind_int(stmt, 1, 235);
sqlite3_bind_text(stmt, 2, "Bar", -1, NULL);
}
if (sqlite3_step(stmt) != SQLITE_DONE)
NSLog(@"This should be real error checking!");
sqlite3_finalize(stmt);

There are multiple bind statements available, depending on the datatype you wish to use. Most bind functions take only three parameters:

· The first parameter to any bind function, regardless of the datatype, is a pointer to the sqlite3_stmt used previously in the sqlite3_prepare_v2() call.

· The second parameter is the index of the variable to which you’re binding. This is a one-indexed value, meaning that the first question mark in the SQL statement has index 1, and each one after it is one higher than the one to its left.

· The third parameter is always the value that should be substituted for the question mark.

A few bind functions, such as those for binding text and binary data, have two additional parameters:

· The first additional parameter is the length of the data being passed in the third parameter. In the case of C strings, you can pass -1 instead of the string’s length, and the function will use the entire string. In all other cases, you need to tell it the length of the data being passed in.

· The final parameter is an optional function callback in case you need to do any memory cleanup after the statement is executed. Typically, such a function would be used to free memory allocated using malloc().

The syntax that follows the bind statements may seem a little odd since we’re doing an insert. When using bind variables, the same syntax is used for both queries and updates. If the SQL string had an SQL query, rather than an update, we would need to call sqlite3_step() multiple times until it returned SQLITE_DONE. Since this is an update, we call it only once.

The SQLite3 Application

In Xcode, create a new project using the Single View Application template and name it SQLite Persistence. This project will start off identical to the previous project, so begin by opening the ViewController.m file, and then make the following changes:

#import "ViewController.h"

@interface ViewController ()

@property (strong, nonatomic) IBOutletCollection(UITextField) NSArray *lineFields;

@end

Next, select Main.storyboard. Design the view and connect the outlet collection by following the instructions in the “Designing the Persistence Application View” section earlier in this chapter. Once your design is complete, save the storyboard file.

We’ve covered the basics, so let’s see how this would work in practice. We’re going to retrofit our Persistence application again, this time storing its data using SQLite3. We’ll use a single table and store the field values in four different rows of that table. We’ll also give each row a row number that corresponds to its field. For example, the value from the first line will get stored in the table with a row number of 0, the next line will be row number 1, and so on. Let’s get started.

Linking to the SQLite3 Library

SQLite 3 is accessed through a procedural API that provides interfaces to a number of C function calls. To use this API, we’ll need to link our application to a dynamic library called libsqlite3.dylib. The process of linking a dynamic library into your project is exactly the same as that of linking in a framework.

Select the SQLite Persistence item at the very top of the Project Navigator list (leftmost pane), and then select SQLite Persistence from the TARGETS section in the main area (see the middle pane of Figure 13-6). (Be careful that you have selected SQLite Persistence from the TARGETSsection, not from the PROJECT section.)

image

Figure 13-6. Selecting the SQLite Persistence project in the Project Navigator; selecting the SQLite Persistence target; and finally, selecting the Build Phases tab

With the SQLite Persistence target selected, click the Build Phases tab in the rightmost pane. You’ll see a list of items, initially all collapsed, which represent the various steps Xcode goes through to build the application. Expand the item labeled Link Binary With Libraries. This section contains the libraries and frameworks that Xcode links with your application. By default, it’s empty because the compiler automatically links with any iOS frameworks that your application uses, but the compiler doesn’t know anything about the SQLite3 library, so we need to add it here.

Click the + button at the bottom of the linked frameworks list, and you’ll be presented with a sheet that lists all available frameworks and libraries. Find libsqlite3.dylib in the list (or use the handy search field) and click the Add button. Note that there may be several other entries in that directory that start with libsqlite3. Be sure you select libsqlite3.dylib. It is an alias that always points to the latest version of the SQLite3 library.

Modifying the Persistence View Controller

Now we can write some more code. Select ViewController.m and make the following changes:

#import "ViewController.h"
#import <sqlite3.h>

@interface ViewController ()

@property (strong, nonatomic) IBOutletCollection(UITextField) NSArray *lineFields;

@end

@implementation ViewController

- (NSString *)dataFilePath {
NSArray *paths = NSSearchPathForDirectoriesInDomains(
NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = [paths objectAtIndex:0];
return [documentsDirectory stringByAppendingPathComponent:@"data.sqlite"];
}

- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
sqlite3 *database;
if (sqlite3_open([[self dataFilePath] UTF8String], &database)
!= SQLITE_OK) {
sqlite3_close(database);
NSAssert(0, @"Failed to open database");
}

// Useful C trivia: If two inline strings are separated by nothing
// but whitespace (including line breaks), they are concatenated into
// a single string:
NSString *createSQL = @"CREATE TABLE IF NOT EXISTS FIELDS "
"(ROW INTEGER PRIMARY KEY, FIELD_DATA TEXT);";
char *errorMsg;
if (sqlite3_exec (database, [createSQL UTF8String],
NULL, NULL, &errorMsg) != SQLITE_OK) {
sqlite3_close(database);
NSAssert(0, @"Error creating table: %s", errorMsg);
}

NSString *query = @"SELECT ROW, FIELD_DATA FROM FIELDS ORDER BY ROW";
sqlite3_stmt *statement;
if (sqlite3_prepare_v2(database, [query UTF8String],
-1, &statement, nil) == SQLITE_OK) {
while (sqlite3_step(statement) == SQLITE_ROW) {
int row = sqlite3_column_int(statement, 0);
char *rowData = (char *)sqlite3_column_text(statement, 1);

NSString *fieldValue = [[NSString alloc]
initWithUTF8String:rowData];
UITextField *field = self.lineFields[row];
field.text = fieldValue;
}
sqlite3_finalize(statement);
}
sqlite3_close(database);

UIApplication *app = [UIApplication sharedApplication];
[[NSNotificationCenter defaultCenter]
addObserver:self
selector:@selector(applicationWillResignActive:)
name:UIApplicationWillResignActiveNotification
object:app];
}

- (void)applicationWillResignActive:(NSNotification *)notification
{
sqlite3 *database;
if (sqlite3_open([[self dataFilePath] UTF8String], &database)
!= SQLITE_OK) {
sqlite3_close(database);
NSAssert(0, @"Failed to open database");
}
for (int i = 0; i < 4; i++) {
UITextField *field = self.lineFields[i];
// Once again, inline string concatenation to the rescue:
char *update = "INSERT OR REPLACE INTO FIELDS (ROW, FIELD_DATA) "
"VALUES (?, ?);";
char *errorMsg = NULL;
sqlite3_stmt *stmt;
if (sqlite3_prepare_v2(database, update, -1, &stmt, nil)
== SQLITE_OK) {
sqlite3_bind_int(stmt, 1, i);
sqlite3_bind_text(stmt, 2, [field.text UTF8String], -1, NULL);
}
if (sqlite3_step(stmt) != SQLITE_DONE) {
NSAssert(0, @"Error updating table: %s", errorMsg);
}
sqlite3_finalize(stmt);
}
sqlite3_close(database);
}

- (void)didReceiveMemoryWarning
{
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}

@end

The first piece of new code to look at is in the viewDidLoad method. We begin by opening the database. If we hit a problem with opening the database, we close it and raise an assertion:

sqlite3 *database;
if (sqlite3_open([[self dataFilePath] UTF8String], &database)
!= SQLITE_OK) {
sqlite3_close(database);
NSAssert(0, @"Failed to open database");
}

Next, we need to make sure that we have a table to hold our data. We can use SQL CREATE TABLE to do that. By specifying IF NOT EXISTS, we prevent the database from overwriting existing data. If there is already a table with the same name, this command quietly completes without doing anything, so it’s safe to call every time our application launches without explicitly checking to see if a table exists:

NSString *createSQL = @"CREATE TABLE IF NOT EXISTS FIELDS "
"(ROW INTEGER PRIMARY KEY, FIELD_DATA TEXT);";
char *errorMsg;
if (sqlite3_exec (database, [createSQL UTF8String],
NULL, NULL, &errorMsg) != SQLITE_OK) {
sqlite3_close(database);
NSAssert(0, @"Error creating table: %s", errorMsg);
}

Each row in the database table contains an integer and a string. The integer is the number of the row in the GUI from which the data was obtained (starting from zero), and the string is the content of the text field on that row. Finally, we need to load our data. We do this using an SQL SELECTstatement. In this simple example, we create an SQL SELECT that requests all the rows from the database and ask SQLite3 to prepare our SELECT. We also tell SQLite3 to order the rows by the row number, so that we always get them back in the same order. Absent this, SQLite3 will return the rows in the order in which they are stored internally.

NSString *query = @"SELECT ROW, FIELD_DATA FROM FIELDS ORDER BY ROW";
sqlite3_stmt *statement;
if (sqlite3_prepare_v2(database, [query UTF8String],
-1, &statement, nil) == SQLITE_OK) {

Next, we step through each of the returned rows:

while (sqlite3_step(statement) == SQLITE_ROW) {

Now we grab the row number, store it in an int, and then grab the field data as a C string:

int row = sqlite3_column_int(statement, 0);
char *rowData = (char *)sqlite3_column_text(statement, 1);

Next, we set the appropriate field with the value retrieved from the database:

NSString *fieldValue = [[NSString alloc]
initWithUTF8String:rowData];
UITextField *field = self.lineFields[row];
field.text = fieldValue;

Finally, we close the database connection, and we’re finished:

}
sqlite3_finalize(statement);
}
sqlite3_close(database);

Note that we close the database connection as soon as we’re finished creating the table and loading any data it contains, rather than keeping it open the entire time the application is running. It’s the simplest way of managing the connection; and in this little app, we can just open the connection those few times we need it. In a more database-intensive app, you might want to keep the connection open all the time.

The other changes we made are in the applicationWillResignActive: method, where we need to save our application data. Our application’s data will look something like Table 13-1 when stored in the database table.

Table 13-1. Data Stored in the FIELDS Table of the Database

ROW

FIELD_DATA

0

Here’s to the crazy ones.

1

The misfits. The rebels.

2

The troublemakers.

3

The round pegs in the square holes.

The applicationWillResignActive: method starts by once again opening the database. To save the data, we loop through all four fields and issue a separate command to update each row of the database:

for (int i = 0; i < 4; i++) {
UITextField *field = self.lineFields[i];

We craft an INSERT OR REPLACE SQL statement with two bind variables. The first represents the row that’s being stored; the second is for the actual string value to be stored. By using INSERT OR REPLACE instead of the more standard INSERT, we don’t need to worry about whether a row already exists:

char *update = "INSERT OR REPLACE INTO FIELDS (ROW, FIELD_DATA) "
"VALUES (?, ?);";

Next, we declare a pointer to a statement, prepare our statement with the bind variables, and bind values to both of the bind variables:

sqlite3_stmt *stmt;
if (sqlite3_prepare_v2(database, update, -1, &stmt, nil)
== SQLITE_OK) {
sqlite3_bind_int(stmt, 1, i);
sqlite3_bind_text(stmt, 2, [field.text UTF8String], -1, NULL);
}

Now we call sqlite3_step to execute the update, check to make sure it worked, and finalize the statement, ending the loop:

if (sqlite3_step(stmt) != SQLITE_DONE) {
NSAssert(0, @"Error updating table: %s", errorMsg);
}
sqlite3_finalize(stmt);

Notice that we used an assertion here to check for an error condition. We use assertions rather than exceptions or manual error-checking because this condition should happen only if we, the developers, make a mistake. Using this assertion macro will help us debug our code, and it can be stripped out of our final application. If an error condition is one that a user might reasonably experience, you should probably use some other form of error checking.

Note There is one condition that could cause an error to occur in the preceding SQLite code that is not a programmer error. If the device’s storage is completely full—to the extent that SQLite can’t save its changes to the database—then an error will occur here, as well. However, this condition is fairly rare and will probably result in deeper problems for the user, outside the scope of our app’s data. Our app probably wouldn’t even launch successfully if the system were in that state. So we’re going to just sidestep the issue entirely.

Once we’re finished with the loop, we close the database:

sqlite3_close(database);

Why don’t you compile and run the app? Enter some data and then press the iPhone simulator’s Home button. Quit the simulator (to force the app to actually quit), and then relaunch the SQLite Persistence application. That data should be right where you left it. As far as the user is concerned, there’s absolutely no difference between the various versions of this application; however, each version uses a very different persistence mechanism.

Using Core Data

The final technique we’re going to demonstrate in this chapter is how to implement persistence using Apple’s Core Data framework. Core Data is a robust, full-featured persistence tool. Here, we will show you how to use Core Data to re-create the same persistence you’ve seen in our Persistence application so far.

Note For more comprehensive coverage of Core Data, check out Pro iOS Persistence: Using Core Data by Michael Privet and Robert Warner (Apress, 2014).

In Xcode, create a new project. Select the Single View Application template from the iOS section and click Next. Name the product Core Data Persistence and select Universal from the Devices control; but don’t click the Next button just yet. If you look just below the Devices control, you’ll see a check box labeled Use Core Data. There’s a certain amount of complexity involved in adding Core Data to an existing project, so Apple has kindly provided an option with some application project templates to do much of the work for you.

Check the Use Core Data check box (see Figure 13-7), and then click the Next button. When prompted, choose a directory to store your project and then click Create.

image

Figure 13-7. Some project templates, including Single View Application, offer the option to use Core Data for persistence

Before we move on to our code, let’s take a look at the project window, which contains some new stuff. Expand the Core Data Persistence folder if it’s closed (see Figure 13-8).

image

Figure 13-8. Our project template with the files needed for Core Data. The Core Data model is selected, and the data model editor is shown in the editing pane

Entities and Managed Objects

Most of what you see in the Project Navigator should be familiar: the application delegate and the image assets catalog. In addition, you’ll find a file called Core_Data_Persistence.xcdatamodeld, which contains our data model. Within Xcode, Core Data lets us design our data models visually, without writing code, and stores that data model in the .xcdatamodeld file.

Single-click the .xcdatamodeld file now, and you will be presented with the data model editor (see the right side of Figure 13-8). The data model editor gives you two distinct views into your data model, depending on the setting of the Editor Style control in the lower-right corner of the project window. In Table mode, the mode shown in Figure 13-8, the elements that make up your data model will be shown in a series of editable tables. In Graph mode, you’ll see a graphical depiction of the same elements. At the moment, both views reflect the same empty data model.

Before Core Data, the traditional way to create data models was to create subclasses of NSObject and conform them to NSCoding and NSCopying so that they could be archived, as we did earlier in this chapter. Core Data uses a fundamentally different approach. Instead of classes, you begin by creating entities here in the data model editor; and then, in your code, you create managed objects from those entities.

Note The terms entity and managed object can be a little confusing, since both refer to data model objects. Entity refers to the description of an object. Managed object refers to actual concrete instances of that entity created at runtime. So, in the data model editor, you create entities; but in your code, you create and retrieve managed objects. The distinction between entities and managed objects is similar to the distinction between a class and instances of that class.

An entity is made up of properties. There are three types of properties:

· Attributes: An attribute serves the same function in a Core Data entity as an instance variable does in an Objective-C class. They both hold the data.

· Relationships: As the name implies, a relationship defines the relationship between entities. For example, to create a Person entity, you might start by defining a few attributes such as hairColor, eyeColor, height, and weight. You might also define address attributes, such as state and zipCode, or you might embed them in a separate HomeAddress entity. Using the latter approach, you would then create a relationship between a Person and a HomeAddress. Relationships can be to-one and to-many. The relationship from Person to HomeAddress is probably to-one, since most people have only a single home address. The relationship from HomeAddress to Person might be to-many, since there may be more than one Person living at that HomeAddress.

· Fetched properties: A fetched property is an alternative to a relationship. Fetched properties allow you to create a query that is evaluated at fetch time to see which objects belong to the relationship. To extend our earlier example, a Person object could have a fetched property called Neighbors that finds all HomeAddress objects in the data store that have the same ZIP code as the Person’s own HomeAddress. Due to the nature of how fetched properties are constructed and used, they are always one-way relationships. Fetched properties are also the only kind of relationship that lets you traverse multiple data stores.

Typically, attributes, relationships, and fetched properties are defined using Xcode’s data model editor. In our Core Data Persistence application, we’ll build a simple entity, so you can get a sense of how this all works together.

Key-Value Coding

In your code, instead of using accessors and mutators, you will use key-value coding to set properties or retrieve their existing values. Key-value coding may sound intimidating, but you’ve already used it quite a bit in this book. Every time we used NSDictionary, for example, we were using a form of key-value coding because every object in a dictionary is stored under a unique key value. The key-value coding used by Core Data is a bit more complex than that used by NSDictionary, but the basic concept is the same.

When working with a managed object, the key you will use to set or retrieve a property’s value is the name of the attribute you wish to set. So, here’s how to retrieve the value stored in the attribute called name from a managed object:

NSString *name = [myManagedObject valueForKey:@"name"];

Similarly, to set a new value for a managed object’s property, do this:

[myManagedObject setValue:@"Gregor Overlander" forKey:@"name"];

Putting It All in Context

So where do these managed objects live? They live in something called a persistent store, also referred to as a backing store. Persistent stores can take several different forms. By default, a Core Data application implements a backing store as an SQLite database stored in the application’sDocuments directory. Even though your data is stored via SQLite, classes in the Core Data framework do all the work associated with loading and saving your data. If you use Core Data, you don’t need to write any SQL statements like the ones you saw in the SQLite Persistence application. You just work with objects, and Core Data figures out what it needs to do behind the scenes.

SQLite isn’t the only option Core Data has for storage. Backing stores can also be implemented as binary flat files or even stored in an XML format. Another option is to create an in-memory store, which you might use if you’re writing a caching mechanism; however, it doesn’t save data beyond the end of the current session. In almost all situations, you should just leave it as the default and use SQLite as your persistent store.

Although most applications will have only one persistent store, it is possible to have multiple persistent stores within the same application. If you’re curious about how the backing store is created and configured, take a look at the file AppDelegate.m in your Xcode project. The Xcode project template we chose provided us with all the code needed to set up a single persistent store for our application.

Other than creating it (which is handled for you in your application delegate), you generally won’t work with your persistent store directly. Rather, you will use something called a managed object context, often referred to as just a context. The context manages access to the persistent store and maintains information about which properties have changed since the last time an object was saved. The context also registers all changes with the undo manager, which means that you always have the ability to undo a single change or roll back all the way to the last time data was saved.

Note You can have multiple contexts pointing to the same persistent store, though most iOS applications will use only one.

Many Core Data method calls require an NSManagedObjectContext as a parameter or must be executed against a context. With the exception of more complicated, multithreaded iOS applications, you can just use the managedObjectContext property provided by your application delegate, which is a default context that is created for you automatically, also courtesy of the Xcode project template.

You may notice that in addition to a managed object context and a persistent store coordinator, the provided application delegate also contains an instance of NSManagedObjectModel. This class is responsible for loading and representing, at runtime, the data model you will create using the data model editor in Xcode. You generally won’t need to interact directly with this class. It’s used behind the scenes by the other Core Data classes, so they can identify which entities and properties you’ve defined in your data model. As long as you create your data model using the provided file, there’s no need to worry about this class at all.

Creating New Managed Objects

Creating a new instance of a managed object is pretty easy, though not quite as straightforward as creating a normal object instance using alloc and init. Instead, you use the insertNewObjectForEntityForName:inManagedObjectContext: factory method in a class calledNSEntityDescription. NSEntityDescription’s job is to keep track of all the entities defined in the app’s data model and to let you create instances of those entities. This method creates and returns an instance representing a single entity in memory. It returns either an instance of NSManagedObject that is set up with the correct properties for that particular entity; or, if you’ve configured your entity to be implemented with a specific subclass of NSManagedObject, an instance of that class. Remember that entities are like classes. An entity is a description of an object and defines which properties a particular entity has.

To create a new object, do this:

NSManagedObject *thing = [NSEntityDescription
insertNewObjectForEntityForName:@"Thing"
inManagedObjectContext:context];

The method is called insertNewObjectForEntityForName:inManagedObjectContext: because, in addition to creating the object, it inserts the newly created object into the context and then returns that object. After this call, the object exists in the context, but is not yet part of the persistent store. The object will be added to the persistent store the next time the managed object context’s save: method is called.

Retrieving Managed Objects

To retrieve managed objects from the persistent store, you’ll use a fetch request, which is Core Data’s way of handling a predefined query. For example, you might say, “Give me every Person whose eyeColor is blue.”

After first creating a fetch request, you provide it with an NSEntityDescription that specifies the entity of the object or objects you wish to retrieve. Here is an example that creates a fetch request:

NSFetchRequest *request = [[NSFetchRequest alloc] init];
NSEntityDescription *entityDescr = [NSEntityDescription
entityForName:@"Thing" inManagedObjectContext:context];
[request setEntity:entityDescr];

Optionally, you can also specify criteria for a fetch request using the NSPredicate class. A predicate is similar to the SQL WHERE clause and allows you to define the criteria used to determine the results of your fetch request. Here is a simple example of a predicate:

NSPredicate *pred =
[NSPredicate predicateWithFormat:@"(name = %@)", nameString];
[request setPredicate: pred];

The predicate created by the first line of code tells a fetch request that, instead of retrieving all managed objects for the specified entity, get just those where the name property is set to the value currently stored in the nameString variable. So, if nameString is an NSString that holds the value @"Bob", we are telling the fetch request to bring back only managed objects that have a name property set to "Bob". This is a simple example, but predicates can be considerably more complex and can use Boolean logic to specify the precise criteria you might need in most any situation.

Note Learn Objective-C on the Mac, 2nd Edition, by Scott Knaster, Waqar Maliq, and Mark Dalrymple (Apress, 2012) has an entire chapter devoted to the use of NSPredicate.

After you’ve created your fetch request, provided it with an entity description, and optionally given it a predicate, you execute the fetch request using an instance method on NSManagedObjectContext:

NSError *error;
NSArray *objects = [context executeFetchRequest:request error:&error];
if (objects == nil) {
// handle error
}

executeFetchRequest:error: will load the specified objects from the persistent store and return them in an array. If an error is encountered, you will get a nil array, and the error pointer you provided will point to an NSError object that describes the specific problem. If no error occurs, you will get a valid array, though it may not have any objects in it since it is possible that none meets the specified criteria. From this point on, any changes you make to the managed objects returned in that array will be tracked by the managed object context you executed the request against, and saved when you send that context a save: message.

The Core Data Application

Let’s take Core Data for a spin now. First, we’ll return our attention to Xcode and create our data model.

Designing the Data Model

Select Core_Data_Persistence.xcdatamodel to open Xcode’s data model editor. The data model editing pane shows all the entities, fetch requests, and configurations that are contained within your data model.

Note The Core Data concept of configurations lets you define one or more named subsets of the entities contained in your data model, which can be useful in certain situations. For example, if you want to create a suite of apps that shares the same data model, but some apps shouldn’t have access to everything (perhaps there’s one app for normal users and another for administrators), this approach lets you do that. You can also use multiple configurations within a single app as it switches between different modes of operation. In this book, we’re not going to deal with configurations at all; but since the list of configurations (including the single default configuration that contains everything in your model) is right there, staring you in the face beneath the entities and fetch requests, we thought it was worth a mention here.

As shown in Figure 13-8, those lists are empty now because we haven’t created anything yet. Remedy that by clicking the plus icon labeled Add Entity in the lower-left corner of the editor pane. This will create a brand-new entity with the name Entity (see Figure 13-9).

image

Figure 13-9. The data model editor, showing our newly added entity

As you build your data model, you’ll probably find yourself switching between Table view and Graph view using the Editor Style control at the bottom right of the editing area. Switch to Graph view now. Graph view presents a little box representing our entity, which itself contains sections for showing the entity’s attributes and relationships, also currently empty (see Figure 13-10). Graph view is really useful if your model contains multiple entities, because it shows a graphic representation of all the relationships between your entities.

image

Figure 13-10. Using the control in the lower-right corner, we switched the data model editor into Graph mode. Note that Graph mode shows the same entities as Table mode, just in a graphic form. This is useful if you have multiple entities with relationships between them

Note If you prefer working graphically, you can actually build your entire model in Graph view. We’re going to stick with Table view in this chapter because it’s easier to explain. When you’re creating your own data models, feel free to work in Graph view if that approach suits you better.

Whether you’re using Table view or Graph view for designing your data model, you’ll almost always want to bring up the Core Data data model inspector. This inspector lets you view and edit relevant details for whatever item is selected in the data model editor—whether it’s an entity, attribute, relationship, or anything else. You can browse an existing model without the data model inspector; but to really work on a model, you’ll invariably need to use this inspector, much as you frequently use the Attributes Inspector when editing nib files.

Press imageimage3 to open the data model inspector. At the moment, the inspector shows information about the entity we just added. The single entity in our model contains the data from one line on the GUI, so we’ll call it Line. Change the Name field from Entity to Line (see Figure 13-11).

image

Figure 13-11. Using the data model inspector to change our entity’s name to Line

If you’re currently in Graph view, use the Editor Style control to switch to Table view now. Table view shows more details for each piece of the entity we’re working on, so it’s usually more useful than Graph view when creating a new entity. In Table view, most of the data model editor is taken up by the table showing the entity’s attributes, relationships, and fetched properties. This is where we’ll set up our entity.

Notice that at the lower right of the editing area, next to the Editor Style control, there’s an icon containing a plus sign labeled Add Attribute. If you select your entity and then hold down the mouse button over this control, a pop-up menu will appear, allowing you to add an attribute, relationship, or fetched property to your entity (see Figure 13-12).

image

Figure 13-12. With an entity selected, press and hold the right plus-sign icon to add an attribute, relationship, or fetched property to your entity

Note Notice that you don’t need to press and hold to add an attribute. You’ll get the same result if you just click the plus icon. Shortcut!

Go ahead and use this technique to add an attribute to your Line entity. A new attribute, creatively named attribute, is added to the Attributes section of the table and selected. In the table, you’ll see that not only is the row selected, but the attribute’s name is selected as well. This means that immediately after clicking the plus sign, you can start typing the name of the new attribute without further clicking.

Change the new attribute’s name from attribute to lineNumber, and click the pop-up next to the name to change its Type from Undefined to Integer 16. Doing so turns this attribute into one that will hold an integer value. We will be using this attribute to identify which of the managed object’s four fields holds data. Since we have only four options, we selected the smallest integer type available.

Now direct your attention to the data model inspector, which is in the pane to the right of the editor area. Here, additional details can be configured. The check box below the Name field on the right, Optional, is selected by default. Click it to deselect it. We don’t want this attribute to be optional—a line that doesn’t correspond to a label on our interface is useless.

Selecting the Transient check box creates a transient attribute. This attribute is used to specify a value that is held by managed objects while the app is running, but is never saved to the data store. We do want the line number saved to the data store, so leave the Transient check box unchecked.

Selecting the Indexed check box will cause an index in the underlying SQL database to be created on the column that holds this attribute’s data. Leave the Indexed check box unchecked. The amount of data is small, and we won’t provide the user with a search capability; therefore, there’s no need for an index.

Beneath that are more settings that allow us to do some simple data validation by specifying minimum and maximum values for the integer, a default value, and more. We won’t be using any of these settings in this example.

Now make sure the Line entity is selected and click the Add Attribute control to add a second attribute. Change the name of your new attribute to lineText and change its Type to String. This attribute will hold the actual data from the text field. Leave the Optional check box checked for this one; it is altogether possible that the user won’t enter a value for a given field.

Note When you change the Type to String, you’ll notice that the inspector shows a slightly different set of options for setting a default value or limiting the length of the string. Although we won’t be using any of those options for this application, it’s nice to know they’re there.

Guess what? Your data model is complete. That’s all there is to it. Core Data lets you point and click your way to an application data model. Let’s finish building the application so you can see how to use our data model from our code.

Creating the Persistence View

Select ViewController.m and make the following change:

#import "ViewController.h"

@interface ViewController ()

@property (strong, nonatomic) IBOutletCollection(UITextField) NSArray *lineFields;

@end

Save this file. Next, select Main.storyboard to edit the GUI in Interface Builder. Design the view and connect the outlet collection by following the instructions in the “Designing the Persistence Application View” section earlier in this chapter. You might also find it useful to refer back toFigure 13-5. Once your design is complete, save the storyboard file.

Now go back to ViewController.m, and make the following changes:

#import "ViewController.h"
#import "AppDelegate.h"

static NSString * const kLineEntityName = @"Line";
static NSString * const kLineNumberKey = @"lineNumber";
static NSString * const kLineTextKey = @"lineText";

@interface ViewController ()

@property (strong, nonatomic) IBOutletCollection(UITextField) NSArray *lineFields;

@end

@implementation ViewController

- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.

AppDelegate *appDelegate = [UIApplication sharedApplication].delegate;
NSManagedObjectContext *context = [appDelegate managedObjectContext];
NSFetchRequest *request = [[NSFetchRequest alloc]
initWithEntityName:kLineEntityName];

NSError *error;
NSArray *objects = [context executeFetchRequest:request error:&error];
if (objects == nil) {
NSLog(@"There was an error!");
// Do whatever error handling is appropriate
}

for (NSManagedObject *oneObject in objects) {
int lineNum = [[oneObject valueForKey:kLineNumberKey] intValue];
NSString *lineText = [oneObject valueForKey:kLineTextKey];

UITextField *theField = self.lineFields[lineNum];
theField.text = lineText;
}

UIApplication *app = [UIApplication sharedApplication];
[[NSNotificationCenter defaultCenter]
addObserver:self
selector:@selector(applicationWillResignActive:)
name:UIApplicationWillResignActiveNotification
object:app];
}

- (void)applicationWillResignActive:(NSNotification *)notification {
AppDelegate *appDelegate = [UIApplication sharedApplication].delegate;
NSManagedObjectContext *context = [appDelegate managedObjectContext];
NSError *error;
for (int i = 0; i < 4; i++) {
UITextField *theField = self.lineFields[i];

NSFetchRequest *request = [[NSFetchRequest alloc]
initWithEntityName:kLineEntityName];
NSPredicate *pred = [NSPredicate
predicateWithFormat:@"(%K = %d)", kLineNumberKey, i];
[request setPredicate:pred];

NSArray *objects = [context executeFetchRequest:request error:&error];
if (objects == nil) {
NSLog(@"There was an error!");
// Do whatever error handling is appropriate
}

NSManagedObject *theLine = nil;
if ([objects count] > 0) {
theLine = [objects objectAtIndex:0];
} else {
theLine = [NSEntityDescription
insertNewObjectForEntityForName:kLineEntityName
inManagedObjectContext:context];
}

[theLine setValue:[NSNumber numberWithInt:i] forKey:kLineNumberKey];
[theLine setValue:theField.text forKey:kLineTextKey];

}
[appDelegate saveContext];
}

- (void)didReceiveMemoryWarning
{
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}

@end

Now let’s look at the viewDidLoad method, which needs to check whether there is any existing data in the persistent store. If there is, it should load the data and populate the fields with it. The first thing we do in that method is get a reference to our application delegate, which we then use to get the managed object context that was created for us:

AppDelegate *appDelegate = [UIApplication sharedApplication].delegate;
NSManagedObjectContext *context = [appDelegate managedObjectContext];

The next order of business is to create a fetch request and pass it the entity name, so it knows which type of objects to retrieve:

NSFetchRequest *request = [[NSFetchRequest alloc]
initWithEntityName:kLineEntityName];

Since we want to retrieve all Line objects in the persistent store, we do not create a predicate. By executing a request without a predicate, we’re telling the context to give us every Line object in the store. We make sure we got back a valid array and log it if we didn’t.

NSError *error;
NSArray *objects = [context executeFetchRequest:request error:&error];
if (objects == nil) {
NSLog(@"There was an error!");
// Do whatever error handling is appropriate
}

Next, we use fast enumeration to loop through the array of retrieved managed objects, pull the lineNum and lineText values from each managed object, and use that information to update one of the text fields on our user interface:

for (NSManagedObject *oneObject in objects) {
int lineNum = [[oneObject valueForKey:kLineNumberKey] intValue];
NSString *lineText = [oneObject valueForKey:kLineTextKey];

UITextField *theField = self.lineFields[lineNum];
theField.text = lineText;
}

Then, just as with all the other applications in this chapter, we register to be notified when the application is about to move out of the active state (either by being shuffled to the background or exited completely), so we can save any changes the user has made to the data:

UIApplication *app = [UIApplication sharedApplication];
[[NSNotificationCenter defaultCenter]
addObserver:self
selector:@selector(applicationWillResignActive:)
name:UIApplicationWillResignActiveNotification
object:app];

Let’s look at applicationWillResignActive: next. We start out the same way as the previous method: by getting a reference to the application delegate and using that to get a pointer to our application’s default context:

AppDelegate *appDelegate = [UIApplication sharedApplication].delegate;
NSManagedObjectContext *context = [appDelegate managedObjectContext];

After that, we go into a loop that executes four times, one time for each text field, and then get a reference to the correct field:

for (int i = 0; i < 4; i++) {
UITextField *theField = self.lineFields[i];

Next, we create our fetch request for our Line entry. We need to find out if there’s already a managed object in the persistent store that corresponds to this field, so we create a predicate that identifies the correct object for the field by using the index of the text field as the record key:

NSFetchRequest *request = [[NSFetchRequest alloc]
initWithEntityName:kLineEntityName];
NSPredicate *pred = [NSPredicate
predicateWithFormat:@"(%K = %d)", kLineNumberKey, i];
[request setPredicate:pred];

Now we execute the fetch request against the context and check to make sure that objects is not nil. If it is nil, there was an error, and we should do whatever error checking is appropriate for our application. For this simple application, we’re just logging the error and moving on:

NSArray *objects = [context executeFetchRequest:request error:&error];
if (objects == nil) {
NSLog(@"There was an error!");
// Do whatever error handling is appropriate
}

After that, we declare a pointer to an NSManagedObject and set it to nil. We do this because we don’t know yet whether we’re going to get a managed object from the persistent store or create a new one. To know this, we check if an object that matched our criteria was returned. If there is one, we load it. If there isn’t one, we create a new managed object to hold this field’s text:

NSManagedObject *theLine = nil;
if ([objects count] > 0) {
theLine = [objects objectAtIndex:0];
} else {
theLine = [NSEntityDescription
insertNewObjectForEntityForName:kLineEntityName
inManagedObjectContext:context];
}

Next, we use key-value coding to set the line number and text for this managed object:

[theLine setValue:[NSNumber numberWithInt:i] forKey:kLineNumberKey];
[theLine setValue:theField.text forKey:kLineTextKey];

Finally, once we’re finished looping, we tell the context to save its changes:
[appDelegate saveContext];

That’s it! Build and run the app to make sure it works. The Core Data version of your application should behave exactly the same as the previous versions.

It may seem that Core Data entails a lot of work; and, for a simple application like this, it doesn’t offer much of an advantage. But in more complex applications, Core Data can substantially decrease the amount of time you spend designing and writing your data model.

Persistence Rewarded

You should now have a solid handle on four different ways of preserving your application data between sessions—five ways if you include the user defaults that you learned how to use in the previous chapter. We built an application that persisted data using property lists and modified the application to save its data using object archives. We then made a change and used the iOS’s built-in SQLite3 mechanism to save the application data. Finally, we rebuilt the same application using Core Data. These mechanisms are the basic building blocks for saving and loading data in almost all iOS applications.