We’ve covered the basics, so let’s see how this would work in practice. We’re going to retrofit our Persistence application one more time, this time storing its data using SQLite3. We’re going to 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.
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 iPhone.
The process of linking a dynamic library into your project is exactly the same as that of link- ing in a framework.
Go back to Xcode, and open the Persistence project, if it’s not still open. Select Frame- works in the Groups & Files pane. Next, select Add to project… from the project menu
now. Then, navigate to /Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/
iPhoneSimulatorX.Y.sdk/usr/lib, and find the file called libsqlite3.dylib. Note that X.Y in iPhoneSimulatorX.Y stands for the major and minor release number of the SDK you are cur- rently using. For example, if you are using SDK 3.0, you’d look for iPhoneSumulator3.0.
When you are prompted, make sure to uncheck the box labeled Copy items into destination group’s folder (if needed). Also, make sure you change Reference Type to Relative to Current SDK. 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.
tip
You can link directly to /usr/lib/libsqlite3.dylib if you choose a Reference Type of Absolute Path. This location is a lot easier to remember, but absolute paths are more fragile and often discouraged. Relative paths are safer and less likely to break in future versions, although in the case of libsqlite3.dylib, it’s probably safe to link with an absolute path.
Next, make the following changes to PersistenceViewController.h:
#import <UIKit/UIKit.h>
#import “/usr/include/sqlite3.h”
#define kFilename @”dataarchive.plist”
#define kDataKey @”Data”
#define kFilename @”data.sqlite3”
@interface PersistenceViewController : UIViewController { UITextField *field1;
UITextField *field2;
UITextField *field3;
UITextField *field4;
sqlite3 *database;
}
@property (nonatomic, retain) IBOutlet UITextField *field1;
@property (nonatomic, retain) IBOutlet UITextField *field2;
@property (nonatomic, retain) IBOutlet UITextField *field3;
@property (nonatomic, retain) IBOutlet UITextField *field4;
- (NSString *)dataFilePath;
- (void)applicationWillTerminate:(NSNotification *)notification;
@end
Download at Boykma.Com
Once again, we change the filename so that we won’t be using the same file that we used in the previous version and so that the file properly reflects the type of data it holds. We also declare an instance variable, database, which will point to our application’s database.
Switch over to PersistenceViewController.m, and make the following changes:
#import “PersistenceViewController.h”
#import “FourLines.h”
@implementation PersistenceViewController
@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];
}
- (void)applicationWillTerminate:(NSNotification *)notification { FourLines *fourLines = [[FourLines 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];
[fourLines release];
[archiver release];
[data release];
for (int i = 1; i <= 4; i++) {
NSString *fieldName = [[NSString alloc]
initWithFormat:@”field%d”, i];
UITextField *field = [self valueForKey:fieldName];
[fieldName release];
char *errorMsg;
char *update = “INSERT OR REPLACE INTO FIELDS (ROW, FIELD_DATA) ➥ VALUES (?, ?);”;
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)
NSAssert1(0, @”Error updating table: %s”, errorMsg);
sqlite3_finalize(stmt);
}
sqlite3_close(database);
}
#pragma mark -
- (void)viewDidLoad {
NSString *filePath = [self dataFilePath];
if ([[NSFileManager defaultManager] fileExistsAtPath:filePath]) {
NSData *data = [[NSMutableData alloc]
initWithContentsOfFile:[self dataFilePath]];
NSKeyedUnarchiver *unarchiver =
[[NSKeyedUnarchiver alloc] initForReadingWithData:data];
FourLines *fourLines = [unarchiver decodeObjectForKey:kDataKey];
[unarchiver finishDecoding];
field1.text = fourLines.field1;
field2.text = fourLines.field2;
field3.text = fourLines.field3;
field4.text = fourLines.field4;
[unarchiver release];
[data release];
}
if (sqlite3_open([[self dataFilePath] UTF8String], &database) != SQLITE_OK) {
sqlite3_close(database);
NSAssert(0, @”Failed to open database”);
}
char *errorMsg;
NSString *createSQL = @”CREATE TABLE IF NOT EXISTS FIELDS ➥ (ROW INTEGER PRIMARY KEY, FIELD_DATA TEXT);”;
if (sqlite3_exec (database, [createSQL UTF8String], NULL, NULL, &errorMsg) != SQLITE_OK) {
sqlite3_close(database);
NSAssert1(0, @”Error creating table: %s”, errorMsg);
}
Download at Boykma.Com
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;
[fieldName release];
[fieldValue release];
}
sqlite3_finalize(statement);
}
UIApplication *app = [UIApplication sharedApplication];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationWillTerminate:) name:UIApplicationWillTerminateNotification object:app];
[super viewDidLoad];
} ...
Let’s take a look at these changes. Hmm?
The first changes we made are in the
applicationWillTerminate: method, where we need to save our application data. Because the data in the database is stored in a table, our applica- tion’s data will look something like Table 11-1 when stored.
To save the data, we loop through all four fields and issue a separate command to update each row of
the database. Here’s our loop, and the first thing we do in the loop is craft a field name so we can retrieve the correct text field outlet. Remember, 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.
Table 11-1. Data Stored in the FIELDS Table of the Database
ROW FIELD_DATA
1 Four score and seven years ago 2 our fathers brought forth on this 3 continent, a new nation,
conceived
4 in Liberty, and dedicated to the
for (int i = 1; i <= 4; i++) {
NSString *fieldName = [[NSString alloc]
initWithFormat:@”field%d”, i];
UITextField *field = [self valueForKey:fieldName];
char *errorMsg;
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 have to worry about whether a row already exists or not.
char *update = “INSERT OR REPLACE INTO FIELDS (ROW, FIELD_DATA) ➥ VALUES (?, ?);”;
Next, we declare a pointer to a statement, then prepare our statement with the bind vari- ables, 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 then finalize the statement and close the database:
if (sqlite3_step(stmt) != SQLITE_DONE)
NSAssert1(0, @”Error updating table: %s”, errorMsg);
sqlite3_finalize(stmt);
}
sqlite3_close(database);
This statement will insert our data into the database if it’s not already there, or it will update the existing row whose row number matches if there already is one:
NSString *update = [[NSString alloc] initWithFormat:
@”INSERT OR REPLACE INTO FIELDS (ROW, FIELD_DATA) ➥ VALUES (%d, ‘%@’);”, i, field.text];
Next, we execute the SQL INSERT OR REPLACE against our database:
char *errorMsg;
char *update = “INSERT OR REPLACE INTO FIELDS (ROW, FIELD_DATA) ➥ VALUES (?, ?);”;
Download at Boykma.Com
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)
NSAssert1(0, @”Error updating table: %s”, errorMsg);
sqlite3_finalize(stmt);
Notice that we’ve used an assertion here if we encountered an error. We use assertions rather than exceptions or manual error checking, because this condition should only happen 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.
Once we’re done with the loop, we close the database, and we’re finished with this method’s changes:
sqlite3_close(database);
The only other new code is in the viewDidLoad method. The first thing we do is open the database. If we hit a problem opening the database, we close it and raise an assertion:
if (sqlite3_open([[self dataFilePath] UTF8String], &database) != SQLITE_OK) {
sqlite3_close(database);
NSAssert(0, @”Failed to open database”);
}
Next, we have 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 overwrit- ing 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.
char *errorMsg;
NSString *createSQL = @”CREATE TABLE IF NOT EXISTS FIELDS ➥ (ROW INTEGER PRIMARY KEY, FIELD_DATA TEXT);”;
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 a SQL SELECT statement. In this simple example, we create a 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 (e.g., 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 do some memory cleanup, and we’re all done:
[fieldName release];
[fieldValue release];
} }
Why don’t you compile and run and try it out? Enter some data, and press the iPhone simu- lator’s home button. Then, relaunch the Persistence application, and on launch, that data should be right where you left it. As far as the user is concerned, there’s absolutely no dif- ference between the four different versions of this application, but each version uses a very different persistence mechanism.
Download at Boykma.Com