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. With the exception of Application Settings (in Chapter 12), so far, every sample application either did not store data or used volatile or nonpersistent storage. Every time one of our 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 Mac 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 need 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 only 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. In order to see this, you'll need to look inside the Library directory contained in your home directory. On Mac OS X 10.6 and earlier, this is no problem, but starting with 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, select Go images Go to Folder… to open a small sheet that prompts you for the name of a directory. Type Library and press enter, and the Finder will take you there.

Within the Library folder, drill down into Application Support/iPhone Simulator/. Within that directory, you'll see a subdirectory for each version of iOS supported by your current Xcode installation. For example, you might see one directory named 4.3 and another named 5.0. Drill down into the directory representing the latest version of iOS supported by your version of Xcode. At this point, you should see four subfolders, including one named Applications (see Figure 13–1).

NOTE: If you've installed multiple versions of the SDK, you may see a few additional folders inside the iPhone Simulator directory, with names indicating the iOS version number they represent. That's perfectly normal.

images

Figure 13–1. The layout of one user's Library/Application Support/iPhone Simulator/5.0/ directory showing the Applications folder

Although this listing represents the simulator, the file structure is similar to what's on the actual device. As is probably obvious, the Applications folder is where iOS stores its applications. If you open the Applications folder, you'll see a bunch of folders and files with names that are long strings of characters. These names are globally unique identifiers (GUIDs) and are generated automatically by Xcode. Each of these folders contains one application and its supporting folders.

If you open one of the application subdirectories, you should see something that looks familiar. You'll find one of the iOS applications you've built, along with three support folders:

  • Documents: Your application stores its data in Documents, with the exception of NSUserDefaults-based preference settings.
  • Library: NSUserDefaults-based preference settings are stored in the Library/Preferences folder.
  • 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 your application does need to take responsibility for deleting the files in tmp once they are no longer needed, to avoid filling up the file system.

Getting the Documents Directory

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 Mac OS X. Many of its available options are designed for Mac 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 objectAtIndex: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 Mac 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 0 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 designed for just that purpose called stringByAppendingPathComponent:.

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.

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();

Then 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 make use of 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 off 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 made use of property lists, most recently when we used a property list to specify our application preferences. Property lists are convenient. They can be edited manually using Xcode or the Property List Editor application. Also, both NSDictionary 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 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: method. The following Objective-C 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 of the objects that you put into the NSArray or NSDictionary are serializable objects from the preceding list, you can write 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 delivered classes from Cocoa Touch that aren't specified in the previous list of serializable objects, 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's Resources 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–2).

images

Figure 13–2. 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, name it Persistence, and make sure to turn off the Use Storyboard option. 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. Expand the Classes folder. Then single-click the BIDViewController.h file, and make the following changes:

#import <UIKit/UIKit.h>

@interface BIDViewController : UIViewController

@property (weak, nonatomic) IBOutlet UITextField *field1;
@property (weak, nonatomic) IBOutlet UITextField *field2;
@property (weak, nonatomic) IBOutlet UITextField *field3;
@property (weak, nonatomic) IBOutlet UITextField *field4;
- (NSString *)dataFilePath;
- (void)applicationWillResignActive:(NSNotification *)notification;
@end

In addition to defining four text field outlets, we've also defined two additional methods. One method, dataFilePath, will create and return the full pathname to our data file by concatenating a file name onto the path for the Documents directory. The other method, applicationWillResignActive: will be called when our application quits and will save data to the plist file. We'll discuss these methods when we edit the persistence classes.

Next, select BIDViewController.xib to edit the GUI.

Designing the Persistence Application View

Once Xcode switches over to Interface Builder mode, click the View icon to open the View window in the nib editing pane. 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 use the horizontal centering blue guideline to line up the label with 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–3 as a guide.

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–3.

images

Figure 13–3. Designing the Persistence application's view

Once you have all four text fields and labels placed, control-drag from the File's Owner icon to each of the four text fields. Connect the topmost text field to the outlet called field1, the next one to field2, the third to field3, and the bottom one to field4. When you have all four text fields connected to outlets, save the changes you made to BIDViewController.xib.

Editing the Persistence Classes

In theproject navigator, select BIDViewController.m, and add the following code at the beginning of the file:

#import "BIDViewController.h"

#define kFilename        @"data.plist"

@implementation BIDViewController
@synthesize field1;
@synthesize field2;
@synthesize field3;
@synthesize field4;

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

.
.
.

Then go down a bit to find the viewDidLoad and viewDidUnload methods, and fill in their contents 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];
        field1.text = [array objectAtIndex:0];
        field2.text = [array objectAtIndex:1];
        field3.text = [array objectAtIndex:2];
        field4.text = [array objectAtIndex:3];
    }

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


}
- (void)viewDidUnload {
    [super viewDidUnload];
    // Release any retained subviews of the main view.
    // e.g. self.myOutlet = nil;
    self.field1 = nil;
    self.field2 = nil;
    self.field3 = nil;
    self.field4 = nil;


}

Finally, add the following new method at the bottom of the file, just before @end:

- (void)applicationWillResignActive:(NSNotification *)notification {
    NSMutableArray *array = [[NSMutableArray alloc] init];
    [array addObject:field1.text];
    [array addObject:field2.text];
    [array addObject:field3.text];
    [array addObject:field4.text];
    [array writeToFile:[self dataFilePath] atomically:YES];
}

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

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

In the viewDidLoad method, we do a few more things. First, we check to see if 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, by copying them in the same order as we saved them, we are always sure to get the correct values in the correct fields.

- (void)viewDidLoad {
    [super viewDidLoad];
    NSString *filePath = [self dataFilePath];
    if ([[NSFileManager defaultManager] fileExistsAtPath:filePath]) {
        NSArray *array = [[NSArray alloc] initWithContentsOfFile:filePath];
        field1.text = [array objectAtIndex:0];
        field2.text = [array objectAtIndex:1];
        field3.text = [array objectAtIndex:2];
        field4.text = [array objectAtIndex:3];
    }
}

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 called addObserver:selector:name:object:. We pass an observer of self, specifying that our BIDViewController instance should be notified. For selector, we pass a selector to the applicationWillResignActive: method, telling the notification center to call that method when the notification is posted. The third parameter, name:, is the name of the notification that we're interested in receiving. The final parameter, object:, 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 the application 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 includes when the user quits the application and (in iOS 4 and later) when the application is pushed to the background, perhaps to later be brought back to the foreground. 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 {
    NSMutableArray *array = [[NSMutableArray alloc] init];
    [array addObject:field1.text];
    [array addObject:field2.text];
    [array addObject:field3.text];
    [array addObject:field4.text];
    [array writeToFile:[self dataFilePath] atomically:YES];
}

This method is pretty simple. We create a mutable array, add the text from each of the four fields to the array, and then write the contents of that array out to a 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, you will never receive the notification that the application is terminating, and your data will not be saved.

NOTE: Starting in iOS 4, 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 iPhone simulator entirely, and then restart your app from Xcode. Quitting the simulator is basically the equivalent of rebooting an iPhone, so when it starts up again, your app will have 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 last part of Chapter 9, when we built the Presidents data model object, you saw an example of the process of loading archived data using NSCoder. 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, like 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 to NSCoding (though there are a few noteworthy exceptions, such as UIImage), archiving is actually 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 is a protocol that allows your object to be copied. Being able to copy an object gives you a lot more flexibility when using data model objects. For example, in the Presidents application in Chapter 9, instead of that complex code we wrote to store changes the user made so we could handle both the Cancel and Save buttons, we could have made a copy of the president object and stored the changes in that copy. If the user tapped Save, we would just copy the changed version over to replace the original version.

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 as NSUserDefaults, 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. We need to implement a method that initializes an object from an NSCoder, allowing us to restore an object that was previously archived. If you are subclassing a class that also conforms to NSCoding, you need to make sure you call encodeWithCoder: on your superclass, meaning your method would look like this instead:

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

Implementing the initWithCoder: method is slightly more complex than implementing encodeWithcoder:. If you are subclassing NSObject 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 of your object's properties, your object is archivable and can be written to and read from archives.

Implementing NSCopying

As we 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 almost always what you want.

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 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.

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];
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];

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 might want to make a copy of your project before continuing.

Implementing the BIDFourLines Class

Once you're ready to proceed and have a copy of your Persistence project open in Xcode, select the Persistence folder and press ImagesN or select File Images NewImages New File… When the new file assistant comes up, select Cocoa Touch, select Objective-C class, and click Next. On the next screen, name the class BIDFourLines, and select NSObject in the Subclass of control. Click Next again. Then choose the Persistence folder to save the files, and 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 BIDFourLines.h, and make the following changes:

#import <Foundation/Foundation.h>

@interface BIDFourLines : NSObject
@interface BIDFourLines : NSObject <NSCoding, NSCopying>

@property (copy, nonatomic) NSString *field1;
@property (copy, nonatomic) NSString *field2;
@property (copy, nonatomic) NSString *field3;
@property (copy, nonatomic) NSString *field4;

@end

This is a very straightforward data model class with four string properties. Notice that we've conformed the class to the NSCoding and NSCopying protocols. Now, switch over to BIDFourLines.m, and add the following code:

#import "BIDFourLines.h"

#define    kField1Key    @"Field1"
#define    kField2Key    @"Field2"
#define    kField3Key    @"Field3"
#define    kField4Key    @"Field4"


@implementation BIDFourLines
@synthesize field1;
@synthesize field2;
@synthesize field3;
@synthesize field4;


#pragma mark NSCoding
- (void)encodeWithCoder:(NSCoder *)encoder {
    [encoder encodeObject:field1 forKey:kField1Key];

    [encoder encodeObject:field2 forKey:kField2Key];
    [encoder encodeObject:field3 forKey:kField3Key];
    [encoder encodeObject:field4 forKey:kField4Key];
}

- (id)initWithCoder:(NSCoder *)decoder {
    if (self = [super init]) {
        field1 = [decoder decodeObjectForKey:kField1Key];
        field2 = [decoder decodeObjectForKey:kField2Key];
        field3 = [decoder decodeObjectForKey:kField3Key];
        field4 = [decoder decodeObjectForKey:kField4Key];
    }
    return self;
}

#pragma mark -
#pragma mark NSCopying
- (id)copyWithZone:(NSZone *)zone {
BIDFourLines *copy = [[[self class] allocWithZone:zone] init];
    copy.field1 = [self.field1 copyWithZone:zone];
    copy.field2 = [self.field2 copyWithZone:zone];
    copy.field3 = [self.field3 copyWithZone:zone];
    copy.field4 = [self.field4 copyWithZone:zone];
    return copy;

}

@end

We just implemented all the methods necessary to conform to NSCoding and NSCopying. We encode all four of our properties in encodeWithCoder: and decode all four of them using the same four key values in initWithCoder:. In copyWithZone:, we create a new BIDFourLines object and copy all four strings to it. See? It's not hard at all.

Implementing the BIDViewController Class

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

#import "BIDViewController.h"
#import "BIDFourLines.h"

#define kFilename         @"data.plist"
#define kFilename         @"archive"
#define kDataKey          @"Data"


@implementation BIDViewController
@synthesize field1;
@synthesize field2;
@synthesize field3;
@synthesize field4;

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

#pragma mark -
- (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];
        field1.text = [array objectAtIndex:0];
        field2.text = [array objectAtIndex:1];
        field3.text = [array objectAtIndex:2];
        field4.text = [array objectAtIndex:3];

        NSData *data = [[NSMutableData alloc]
            initWithContentsOfFile:[self dataFilePath]];
        NSKeyedUnarchiver *unarchiver = [[NSKeyedUnarchiver alloc]
            initForReadingWithData:data];
        BIDFourLines *fourLines = [unarchiver decodeObjectForKey:kDataKey];
        [unarchiver finishDecoding];


        field1.text = fourLines.field1;
        field2.text = fourLines.field2;
        field3.text = fourLines.field3;
        field4.text = fourLines.field4;

    }

    UIApplication *app = [UIApplication sharedApplication];
    [[NSNotificationCenter defaultCenter] addObserver:self
              selector:@selector(applicationWillResignActive:)
              name:UIApplicationWillResignActiveNotification
              object:app];
}
.
.
.
- (void)applicationWillResignActive:(NSNotification *)notification {
    NSMutableArray *array = [[NSMutableArray alloc] init];
    [array addObject:field1.text];
    [array addObject:field2.text];
    [array addObject:field3.text];
    [array addObject:field4.text];
    [array writeToFile:[self dataFilePath] atomically:YES];

    BIDFourLines *fourLines = [[BIDFourLines alloc] init];
    fourLines.field1 = field1.text;
    fourLines.field2 = field2.text;
    fourLines.field3 = field3.text;
    fourLines.field4 = field4.text;


    NSMutableData *data = [[NSMutableData alloc] init];
    NSKeyedArchiver *archiver = [[NSKeyedArchiver alloc]
        initForWritingWithMutableData:data];
    [archiver encodeObject:fourLines forKey:kDataKey];
    [archiver finishEncoding];
    [data writeToFile:[self dataFilePath] atomically:YES];
}
...

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. Then we redefined the loading and saving, using BIDFourLines to hold the data, and using the 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 think back to the last example in Chapter 9, where we allowed the user to edit a list of presidents, and each president had four different fields that could be edited. To handle archiving that list of presidents with a property list would involve iterating through the list of presidents, creating an NSDictionary instance for each president, copying the value from each of their fields over to the NSDictionary instance, and adding that instance to another array, which could then be written to a plist file. And that's assuming that we restricted ourselves to using only serializable properties. If we didn't, using property list serialization wouldn't even be an option without doing a lot of conversion work.

On the other hand, if we had an array of archivable objects, such as the BIDFourLines 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.

In other words, this 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.

For example, if your application needs to calculate the sum of a particular field across all the objects in your application, or if you need the sum from just the objects that meet certain criteria, SQLite3 allows you to do that 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). SQL is 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 you'll see 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” (http://www.sqlite.org/cintro.html) and “SQL As Understood by SQLite” (http://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.

In this chapter, 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, such a simple application 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, it will create a new one. Here's what the code to open a new 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 Cstring 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 Images
    (ID INTEGER PRIMARY KEY AUTOINCREMENT, FIELD_DATA TEXT)";
int result = sqlite3_exec(database, createSQL, NULL, NULL, &errorMsg);

As you did 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 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. 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, but 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. Then 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 was an update, we call it only once.

The SQLite3 Application

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 give each row a row number that corresponds to its field, so for example, the value from field1 will get stored in the table with a row number of 1. 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, located in /usr/lib on both Mac OS X and iOS. The process of linking a dynamic library into your project is exactly the same as that of linking in a framework.

Use the Finder to make a copy of your last Persistence project directory, and then open the new copy's .xcodeproj file. Select the Persistence item at the very top of the project navigator's list (leftmost pane), and then select Persistence from the TARGETS section in the main area (middle pane; see Figure 13–4). Be careful that you have selected Persistence from the TARGETS section, and not from the PROJECT section.

images

Figure 13–4. Selecting the Persistence project in the project navigator, then selecting the Persistence target, and finally, selecting the Build Phases tab

With the 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. You'll see the standard frameworks that our application is set up to link with by default: UIKit.framework, Foundation.framework, and CoreGraphics.framework.

Now, let's add the SQLite3 library to our project. 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. Adding this to the project puts it at the top level of the project navigator. For the sake of keeping things organized, you may want to drag it to the project's Frameworks folder.

Modifying the Persistence View Controller

Now, it's time to change things around again. This time, we'll replace the NSCoding code with its SQLite equivalent. Once again, we'll change the file name so that we won't be using the same file that we used in the previous version, and the file name properly reflects the type of data it holds. Then we're going to change the methods that save and load the data.

Select BIDViewController.m, and make the following changes:

#import "BIDViewController.h"
#import "BIDFourLines.h"
#import <sqlite3.h>

#define kFilename    @"archive.plist"
#define kDataKey     @"Data"
#define kFilename    @"data.sqlite3"

@implementation BIDViewController
@synthesize field1;
@synthesize field2;
@synthesize field3;
@synthesize field4;

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

#pragma mark -
- (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])
    {
        NSData *data = [[NSMutableData alloc]
                initWithContentsOfFile:[self dataFilePath]];
        NSKeyedUnarchiver *unarchiver =
                [[NSKeyedUnarchiver alloc] initForReadingWithData:data];
        BIDFourLines *fourLines = [unarchiver decodeObjectForKey:kDataKey];
        [unarchiver finishDecoding];

        field1.text = fourLines.field1;
        field2.text = fourLines.field2;
        field3.text = fourLines.field3;
        field4.text = fourLines.field4;
    }
    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 *fieldName = [[NSString alloc]
                 initWithFormat:@"field%d", row];
             NSString *fieldValue = [[NSString alloc]
                 initWithUTF8String:rowData];
             UITextField *field = [self valueForKey:fieldName];
             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 {
    BIDFourLines *fourLines = [[BIDFourLines alloc] init];
    fourLines.field1 = field1.text;
    fourLines.field2 = field2.text;
    fourLines.field3 = field3.text;
    fourLines.field4 = field4.text;

    NSMutableData *data = [[NSMutableData alloc] init];
    NSKeyedArchiver *archiver = [[NSKeyedArchiver alloc]
            initForWritingWithMutableData:data];
    [archiver encodeObject:fourLines forKey:kDataKey];
    [archiver finishEncoding];
    [data writeToFile:[self dataFilePath] atomically:YES];

    sqlite3 *database;
    if (sqlite3_open([[self dataFilePath] UTF8String], &database)
        != SQLITE_OK) {
        sqlite3_close(database);
        NSAssert(0, @"Failed to open database");
    }
    for (int i = 1; i <= 4; i++) {
        NSString *fieldName = [[NSString alloc]
                               initWithFormat:@"field%d", i];
        UITextField *field = [self valueForKey:fieldName];


        // Once again, inline string concatenation to the rescue:
        char *update = "INSERT OR REPLACE INTO FIELDS (ROW, FIELD_DATA)"

                       "VALUES (?, ?);";
        char *errorMsg;
        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);

}
.
.
.

The first new code 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 exits 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);
    NSAssert1(0, @"Error creating table: %s", errorMsg);
}

Finally, we need to load our data. We do this using an SQL SELECT statement. 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) {

Then we step through each of the returned rows:

        while (sqlite3_step(statement) == SQLITE_ROW) {

We grab the row number and store it in an int, and then we 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 create a field name based on the row number (such as field1 for row 1), convert the C string to an NSString, and use that to set the appropriate field with the value retrieved from the database.

            NSString *fieldName = [[NSString alloc]
                initWithFormat:@"field%d", row];
            NSString *fieldValue = [[NSString alloc]
                initWithUTF8String:rowData];
            UITextField *field = [self valueForKey:fieldName];
            field.text = fieldValue;

Finally, we close the database connection, and we're all 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. Because the data in the database is stored in a table, our application's data will look something like Table 13–1 when stored.

Images

The applicationWillResignActive: method starts off by once again opening the database.

    sqlite3 *database;
    if (sqlite3_open([[self dataFilePath] UTF8String], &database)
        != SQLITE_OK) {
        sqlite3_close(database);
        NSAssert(0, @"Failed to open 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 = 1; i <= 4; i++) {
        NSString *fieldName = [[NSString alloc]
            initWithFormat:@"field%d", i];
        UITextField *field = [self valueForKey:fieldName];

The first thing we do in the loop is craft a field name so we can retrieve the correct text field outlet. Remember that valueForKey: allows you to retrieve a property based on its name. We also declare a pointer to be used for the error message if we encounter an error.

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);
        }

Then 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.");
        }
        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. Probably our app 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 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, but 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 More iOS 5 Development by Alex Horovitz and Kevin Kim (Apress, 2011). That book devotes several chapters to Core Data.

In Xcode, create a new project. This time, select the Empty Application template and click Next. Name the product C ore Data Persistence, and select iPhone from the Device Family popup, but don't click the Next button just yet. If you look just below the Device Family popup, you should see a checkbox 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 checkbox  (see Figure 13–5), and then click the Next button. When prompted, choose a directory to store your project, and click Create.

images

Figure 13–5. Some project templates, including Empty 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 Date Persistence and Supporting Files folders (see Figure 13–6).

images

Figure 13–6. 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 various files in the Supporting Files folder. 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–6). The data model editor gives you two distinct views into your data model, depending on the setting of the control in the lower-right corner of the project window. In Table mode, the mode shown in Figure 13–6, 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 create 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, like hairColor, eyeColor, height, and weight. You might define address attributes, like state and ZIP code, or you might embed those 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 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's Documents 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. 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, but 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 BIDAppDelegate.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, but rather 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, meaning 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. You can find out more about using multiple contexts and the undo manager in the Apress book More iOS 5 Development.

Many Core Data method calls require an NSManagedObjectContext as a parameter or must be executed against a context. With the exception of very 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 called NSEntityDescription. NSEntityDescription's job is to keep track of all the entities defined in the app's data model. This method 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:

    theLine = [NSEntityDescription
        insertNewObjectForEntityForName:@"EntityName"
                 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 make use of 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:@"EntityName" 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, retrieve 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 by Mark Dalrymple and Scott Knaster (Apress, 2009) 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. Otherwise, you will get a valid array, though it may not have any objects in it, since it is possible that none meet 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 share the same data model, but some apps shouldn't have access to everything (perhaps there's one app for normal users and another for sysadmins), 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–6, 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 entity pane. This will create a brand-new entity with the name Entity (see Figure 13–7).

images

Figure 13–7. 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 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–8). Graph view is really useful if your model contains multiple entities, since it shows a graphic representation of all the relationships between your entities.

images

Figure 13–8. 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 like you frequently use the attributes inspector when editing nib files.

Press Style for menu shortcutto open the data model inspector. At the moment, the inspector shows information about the entity we just added. Change the Name field from Entity to Line (see Figure 13–9).

images

Figure 13–9. Using the data model inspector to change our entity's name to Line

If you're currently in Graph view, 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, there's an icon containing a plus sign, similar to the one at the lower left, which you used to create the entity. If you select your entity, and then click the plus sign and hold down the mouse button, a popup menu will appear, allowing you to add an attribute, relationship, or fetched property to your entity (see Figure 13–10).

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

images

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

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 lineNum, and click the popup next to the name to change its Type from Undefined to Int16, which turns this attribute into one that will hold an integer value. We will be using this attribute to identify for which of the four fields the managed object holds data. Since we have only four options, we selected the smallest integer type available.

Now direct your attention to the data model inspector, where additional details can be configured. The checkbox 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 checkbox creates a transient attribute, which is used to specify a value that is held by managed objects while the app is running, but never saved to the data store. Since we do want the line number saved to the data store, leave the Transient checkbox unchecked.

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

Beneath that are more settings, allowing you 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 plus sign 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 checkbox 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 and Controller

Because we selected the Empty Application template, we weren't provided with a view controller. Go back to the project navigator, single-click the Core Data Persistence folder, and press ImagesN or select File Images New Images New File… to bring up the new file assistant. Select UIViewController subclass from the Cocoa Touch heading, and click Next. On the next sheet, name the class BIDViewController, select a Subclass ofUIViewController, make sure the box labeled Targeted for iPad is unchecked, and check the box that says With XIB for user interface to have Xcode create a nib file automatically. Click Next, and choose the directory in which to save the file. When you're finished, BIDViewController.h, BIDViewController.m, and BIDViewController.xib will be placed in your Core Data Persistence folder.

Select BIDViewController.h, and make the following changes, which should look very familiar to you:

#import <UIKit/UIKit.h>

@interface BIDViewController : UIViewController

@property (weak, nonatomic) IBOutlet UITextField *line1;
@property (weak, nonatomic) IBOutlet UITextField *line2;
@property (weak, nonatomic) IBOutlet UITextField *line3;
@property (weak, nonatomic) IBOutlet UITextField *line4;

@end

Save this file. Next, select BIDViewController.xib to edit the GUI in Interface Builder. Design the view, and connect the outlets by following the instructions in the “Designing the Persistence Application View” section earlier in this chapter. Once your design is complete, select the view, bring up the attributes inspector, and select Light Gray Color from the Background popup menu. You might also find it useful to refer back to Figure 13–3. Once you've created the view, save the nib file.

In BIDViewController.m, insert the following code at the top of the file:

#import "BIDViewController.h"
#import "BIDAppDelegate.h"

@implementation BIDViewController
@synthesize line1;
@synthesize line2;
@synthesize line3;
@synthesize line4;

Then insert the following code into the existing viewDidLoad and viewDidUnload methods:

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view from its nib.
    BIDAppDelegate *appDelegate =
        [[UIApplication sharedApplication] delegate];
    NSManagedObjectContext *context = [appDelegate managedObjectContext];
    NSEntityDescription *entityDescription = [NSEntityDescription
                 entityForName:@"Line"
        inManagedObjectContext:context];
    NSFetchRequest *request = [[NSFetchRequest alloc] init];
    [request setEntity:entityDescription];


    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) {
        NSNumber *lineNum = [oneObject valueForKey:@"lineNum"];
        NSString *lineText = [oneObject valueForKey:@"lineText"];


        NSString *fieldName = [NSString
            stringWithFormat:@"line%d", [lineNum integerValue]];
        UITextField *theField = [self valueForKey:fieldName];
        theField.text = lineText;
    }


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

}

- (void)viewDidUnload {
    [super viewDidUnload];
    // Release any retained subviews of the main view.
    // e.g. self.myOutlet = nil;
    self.line1 = nil;
    self.line2 = nil;
    self.line3 = nil;
    self.line4 = nil;

}

Then add the following new method down at the bottom, just before the @end marker:

- (void)applicationWillResignActive:(NSNotification *)notification {
    BIDAppDelegate *appDelegate =[[UIApplication sharedApplication] delegate];
    NSManagedObjectContext *context = [appDelegate managedObjectContext];
    NSError *error;
    for (int i = 1; i <= 4; i++) {
        NSString *fieldName = [NSString stringWithFormat:@"line%d", i];
        UITextField *theField = [self valueForKey:fieldName];


        NSFetchRequest *request = [[NSFetchRequest alloc] init];

        NSEntityDescription *entityDescription = [NSEntityDescription
            entityForName:@"Line"
            inManagedObjectContext:context];
        [request setEntity:entityDescription];
        NSPredicate *pred = [NSPredicate
            predicateWithFormat:@"(lineNum = %d)", i];
        [request setPredicate:pred];


        NSManagedObject *theLine = nil;

        NSArray *objects = [context executeFetchRequest:request
            error:&error];


        if (objects == nil) {
            NSLog(@"There was an error!");
            // Do whatever error handling is appropriate
        }
        if ([objects count] > 0)
            theLine = [objects objectAtIndex:0];
        else
            theLine = [NSEntityDescription
                   insertNewObjectForEntityForName:@"Line"
                            inManagedObjectContext:context];


        [theLine setValue:[NSNumber numberWithInt:i] forKey:@"lineNum"];
        [theLine setValue:theField.text forKey:@"lineText"];


    }
    [context save:&error];
}

.
.
.

Now, let's look at the viewDidLoad method, which needs to check if 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 to get a reference to our application delegate, which we then use to get the managed object context that was created for us.

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

Next, we create an entity description that describes our entity.

    NSEntityDescription *entityDescription = [NSEntityDescription
                 entityForName:@"Line"
    inManagedObjectContext:context];

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

    NSFetchRequest *request = [[NSFetchRequest alloc] init];
    [request setEntity:entityDescription];

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.

    NSError *error;
    NSArray *objects = [context executeFetchRequest:request error:&error];

We make sure we got back a valid array, and log it if we didn't.

    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 it, and use that information to update one of the text fields on our user interface.

    for (NSManagedObject *oneObject in objects) {
        NSNumber *lineNum = [oneObject valueForKey:@"lineNum"];
        NSString *lineText = [oneObject valueForKey:@"lineText"];

        NSString *fieldName = [NSString stringWithFormat:@"line%@",
            lineNum];
        UITextField *theField = [self valueForKey:fieldName];
        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 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];
    [super viewDidLoad];

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.

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

After that, we go into a loop that executes four times, one time for each label.

    for (int i = 1; i <= 4; i++) {

We construct the name of one of the four fields by appending i to the word line and use that to get a reference to the correct field using valueForKey:.

        NSString *fieldName = [NSString stringWithFormat:@"line%d", i];
        UITextField *theField = [self valueForKey:fieldName];

Next, we create our fetch request:

        NSFetchRequest *request = [[NSFetchRequest alloc] init];

After that, we create an entity description that describes the Line entity we designed earlier in the data model editor and that uses the context we retrieved from the application delegate. Once we create the description, we feed it to the fetch request, so the request knows which type of entity to look for.

        NSEntityDescription *entityDescription = [NSEntityDescription
                     entityForName:@"Line"
        inManagedObjectContext:context];
        [request setEntity:entityDescription];

Next, 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.

        NSPredicate *pred = [NSPredicate
            predicateWithFormat:@"(lineNum = %d)", i];
        [request setPredicate:pred];

After that, we declare a pointer to an NSManagedObject and set it to nil. We do this because we don't know yet if we're going to load a managed object from the persistent store or create a new one. We also declare an NSError that the system will use to notify us of the specific nature of the problem if we get back a nil array.

        NSManagedObject *theLine = nil;
        NSError *error;

Next, we execute the fetch request against the context.

        NSArray *objects = [context executeFetchRequest:request
            error:&error];

Then we check to make sure that objects is not nil. If it is nil, then 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.

        if (objects == nil) {
            NSLog(@"There was an error!");
            // Do whatever error handling is appropriate
        }

After that, 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.

        if ([objects count] > 0)
            theLine = [objects objectAtIndex:0];
        else
            theLine = [NSEntityDescription
                insertNewObjectForEntityForName:@"Line"
                         inManagedObjectContext:context];

Then we use key-value coding to set the line number and text for this managed object.

         [theLine setValue:[NSNumber numberWithInt:i] forKey:@"lineNum"];
         [theLine setValue:theField.text forKey:@"lineText"];
}

Finally, once we're finished looping, we tell the context to save its changes.

    [context save:&error];
}
Making the Persistence View Controller the Application's Root Controller

Because we used the Empty Application template instead of the Single View Application template, we have one more step to take before our fancy new Core Data application will work. We need to create an instance of BIDViewController to act as our application's root controller and add its view as a subview of our application's main window. Let's do that now.

First, the application delegate needs a property to point to our view controller. Select BIDAppDelegate.h, and make the following changes to declare that property:

#import <UIKit/UIKit.h>

@class BIDViewController;

@interface BIDAppDelegate : UIResponder <UIApplicationDelegate>

@property (strong, nonatomic) IBOutlet UIWindow *window;

@property (readonly, strong, nonatomic)NSManagedObjectContext
    *managedObjectContext;
@property (readonly, strong, nonatomic)NSManagedObjectModel
    *managedObjectModel;
@property (readonly, strong, nonatomic)NSPersistentStoreCoordinator
    *persistentStoreCoordinator;
@property (strong, nonatomic) BIDViewController *rootController;

- (void)saveContext;
- (NSURL *)applicationDocumentsDirectory;

@end

To make the root controller's view a subview of the application's window so that the user can interact with it, switch to BIDAppDelegate.m, and make the following changes at the top of that file:

#import "BIDAppDelegate.h"
#import "BIDViewController.h"

@implementation BIDAppDelegate

@synthesize window = _window;
@synthesize managedObjectContext = __managedObjectContext;
@synthesize managedObjectModel = __managedObjectModel;
@synthesize persistentStoreCoordinator = __persistentStoreCoordinator;
@synthesize rootController;

- (BOOL)application:(UIApplication *)application
    didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
    // Override point for customization after application launch.
    self.rootController = [[BIDViewController alloc]
                           initWithNibName:@"BIDViewController" bundle:nil];


    UIView *rootView = self.rootController.view;
    CGRect viewFrame = rootView.frame;
    viewFrame.origin.y += [UIApplication
        sharedApplication].statusBarFrame.size.height;
    rootView.frame = viewFrame;
    [self.window addSubview:rootView];

    self.window.backgroundColor = [UIColor whiteColor];
    [self.window makeKeyAndVisible];
    return YES;
}
.
.
.

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, 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.

Ready for more? In the next chapter, we're going to continue talking about saving and loading data, and introduce you to iOS 5's document system. This system not only provides a nice abstraction for dealing with saving and loading documents in files stored on your device, but it also lets you save your documents to Apple's iCloud, one of the biggest new features in iOS 5.

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

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