Bookmark and Share

iPhoneSDK Tutorial
Chapter 10. Data Persistence





10.7 Archiving Persistence

Let's create another project, this time, using archiving mechanism.
Name it "Archiving Persistence" with view-based template. Then make a new file with Cocoa Touch Class, Objective-C class, and with a Subclass of NSObject.

FourLines

Here is the interface file, "FourLines.h"

#import <UIKit/UIKit.h>

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

@interface FourLines : NSObject <NSCoding, NSCopying> {
    NSString *field1;
    NSString *field2;
    NSString *field3;
    NSString *field4;    
}
@property (nonatomic, retain) NSString *field1;
@property (nonatomic, retain) NSString *field2;
@property (nonatomic, retain) NSString *field3;
@property (nonatomic, retain) NSString *field4;
@end

Note that we've conformed the class to the NSCoding and NSCopying protocols.

Now, the implementation file, "FourLines.m"

#import "FourLines.h"

@implementation FourLines
@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]) {
        self.field1 = [decoder decodeObjectForKey:kField1Key];
        self.field2 = [decoder decodeObjectForKey:kField2Key];
        self.field3 = [decoder decodeObjectForKey:kField3Key];
        self.field4 = [decoder decodeObjectForKey:kField4Key];
    }
    return self;
}

#pragma mark -
#pragma mark NSCopying

- (id)copyWithZone:(NSZone *)zone {
    FourLines *copy = [[[self class] allocWithZone: zone] init];
    copy.field1 = [[self.field1 copyWithZone:zone] autorelease];
    copy.field2 = [[self.field2 copyWithZone:zone] autorelease];
    copy.field3 = [[self.field3 copyWithZone:zone] autorelease];
    copy.field4 = [[self.field4 copyWithZone:zone] autorelease];
    
    return copy;
}
@end

We've 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 created a new FourLines object and copy all four strings to it.

What we've got so far?
We have an archivable data object!
Let's use it to persist our application data. Let make some changes to "Archiving_PersistenceViewController.h"

#import <UIKit/UIKit.h>
	
#define kFilename        @"archive"
#define kDataKey         @"Data"
	
@interface Archiving_PersistenceViewController : UIViewController {
		UITextField *field1;
		UITextField *field2;
		UITextField *field3;
		UITextField *field4;    
}
@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

Here, we are specifying a new filename so that our program doesn't try to load the old property list in as an archive. We're also defining a new constant that will be the key value we use to encode and decode our object.

Let's look at implementation file, "Archiving_PersistenceViewController.m"

#import "Archiving_PersistenceViewController.h"
#import "FourLines.h"

@implementation Archiving_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];    
}

#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];        
    }
    
    UIApplication *app = [UIApplication sharedApplication];
    [[NSNotificationCenter defaultCenter] addObserver:self
                                selector:@selector(applicationWillTerminate:)
                                name:UIApplicationWillTerminateNotification 
                                object:app];
    [super viewDidLoad];
}

- (void)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;
    [super viewDidUnload];
}

- (void)dealloc {
    [field1 release];
    [field2 release];
    [field3 release];
    [field4 release];
    [super dealloc];
}
@end

Build and Run. We get the same results as in the example using pList.



10.8 SQLite3

The third option for persistence, SQLite3, is very efficient at storing and retrieving large amounts of data. It's also capable of doing complex aggregations on our data. Getting aggregations from SQLite3 is several orders of magnitude faster than loading all the objects into memory. Being a full-fledged embedded database, SQLite3 contains tools to make it even faster by creating table indexes that can be speed up your queries.

SQLite3 is based on the Structured Query Language(SQL). SQL is the standard language used to interact with relational database. It is a language with its own syntax.

Relational databases and object-oriented programming language 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 languages have arisen. These different techniques are collectively called object relational mapping (ORM).


10.8.1 Creating/Opening Database

Before we can use SQLite3, we need to learn how to open the database. The command, sqlite3_open(), will open an existing database. If none exists, it will create a new database. Here's code used to open a new database:

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. SQLite3 is written in portable C. So, the path to the database file should be passed in C string not NSString. But we have a NSString method that generates a C string from NSString instance:

char *cstringPath = [pathString UTF8String];

When we're done with SQLite3 database, we close the database by calling

sqlite3_close(database);


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

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

The function sqlite3_exec is used to run any command against SQLITE3 that doesn't return data, It's used for updates, inserts, and deletes. Retrieving data form the database is a little more involved. We should first have to prepare the statement by feeding it our 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);

If result equals SQLITE_OK, our statement was successfully prepared, and we can start stepping through the result set.


10.8.2 Bind Variables

It is common practice to use bind variables when inserting into a database. Handling strings correctly, making sure they don't have invalid characters and that quotes are handled property can be quite a chore. With bind variables, those issues are taken care of for us. To insert a value using a bind variable, we create our SQL string. Each question mark represents one variable that has to be bound before the statement can be executed. Then we prepare the SQL statement, bind a value to each of the variables, and then execute the command.

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 we want to use. The first parameter to any bind function, regardless of which datatype it is for, is a pointer to the sqlite3_stmt used previously in the sqlite3_prepare_v2() call. The second parameter is the index of the variable that we're binding to. 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 b substituted for the question mark. Most binding functions only take three parameters. The first is the length of the data being passed in the third parameter. In the case of C strings, we can pass -1 instead of the string's length, and the function will use the entire string. In all other cases, we have to tell it the length of the data being passed in. The final parameter is an optional function callback in case we need to do any memory cleanup after the statement is executed.

The syntax that follows the bind statement 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 a SQL query, rather than an update, we would need to call sqlite3_step() called multiple times, until it returns SQLITE_DONE. Since this was an update, we call it only once.



10.9 SQLite3 Persistence

We're going to create a new Persistence application 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're going to give each row a row number that corresponds to its field.

SQlite 3 is accessed through a procedural API that provides interface to a number of C function calls. To use this, we're going to need to link our application to a dynamic library, libsqlite3.dylib, located in /usr/lib on both Mac OS X and iPhone.

The process of lining a dynamic library into our project is exactly the same as that of linking in a framework.

Go to Xcode, create a new view-based project, and name it "SQLite Persistence." Setup outlets and put four Labels and Text Field on the view of SQLite3_PersistenceViewController. Then back to Xcode.

Select Frameworks in the Group & Files pane. Next, select Add to Project... from the Project menu. Then, navigate to
/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulatorX.Y.sdk/usr/lib, and find the file called lib sqlite3.dylib.

libsqlite3_dylib

When prompted, make sure to uncheck the box labeled Copy items into destination group's folder.

Now, let's look at our interface file, "SQLite3_PersistenceViewController.h":

#import <UIKit/UIKit.h>
#import "/usr/include/sqlite3.h"

#define kFilename    @"data.sqlite3"
@interface SQLite3_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

Then, implementation file, "SQLite3_PersistenceViewController.m":

#import "SQLite3_PersistenceViewController.h"
#import "FourLines.h"

@implementation SQLite3_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 {
    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 {
    
    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);
    }
    
    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];
}

- (void)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;
    [super viewDidUnload];
}

- (void)dealloc {
    [field1 release];
    [field2 release];
    [field3 release];
    [field4 release];
    [super dealloc];
}
@end

Let's look at methods one by one.
In the method, applicationWillTerminate:, we are saving our application data. Because the data in the database is stored in a table, our application's data will look something like the table below.

Row FIELD_DATA
1 We are making
2 a persistence
3 project
4 using SQLite 3

To save the data, we loop through all four fields and issue a separate command to update each row of the database. The first thing we do in the loop is craft a field name so we can retrieve the correct txt field outlet. The valueForKey: allows us to retrieve a property based on its name. We also declared a pointer to be used for the error message if we encounter an error.

- (void)applicationWillTerminate:(NSNotification *)notification {
    for (int i = 1; i <= 4; i++)
    {
        NSString *fieldName = [[NSString alloc]
                               initWithFormat:@"field%d", i];
        UITextField *field = [self valueForKey:fieldName];
        [fieldName release];
        
        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 (?, ?);";

Then, we declared a pointer to a statement, then 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);
        }

Next, we call sqlite3_step to execute the update. Then, we 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];

Then, we execute the SQL INSERT OR REPLACE agains our database:

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

Once we're done with the loop, we close the database.

    sqlite3_close(database);

In the method, viewDidLoad, we 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");
    }

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

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

Next, we need to load our data. We do this using a SQL SELECT statement. I this simple example, we create a SQL_SELECT that request 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, convert the C string to an NSString, and use that to set the appropriate field with the value retrieved form the database:

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

Don't forget we need additional files: FourLines.m and FourLines.h because we started it from scratch instead of retrofitting the previous project.

FilesSQList3

Build and Run.

We should get the same result as in the previous two projects.



10.10 Core Data


I will come back later for this section.



Previous Sections.