If the user creates data while running your application, you
may need a place to store the data so that it’s there the next
time the user runs it. You’ll also want to store user preferences,
passwords, and many other forms of data. You could store data online
somewhere, but then your application won’t function unless it’s online.
The iPhone can store data in lots of ways.
1. Using Flat Files
So-called flat files are files that contain
data, but are typically not backed by the power of a full-featured
database system. They are useful for storing small bits of text data,
but they lack the performance and organizational advantages that a
database provides.
Applications running on the iPhone or iPod touch are sandboxed; you can access only a
limited subset of the filesystem from your application. If you want to
save files from your application, you should save them into the
application’s Document directory.
Here’s the code you need to locate the application’s Document
directory:
NSArray *arrayPaths = NSSearchPathForDirectoriesInDomains(
NSDocumentDirectory, NSUserDomainMask, YES);
NSString *docDirectory = [arrayPaths objectAtIndex:0];
1.1. Reading and writing text content
The NSFileManager
methods generally deal with NSData objects.
For writing to a file, you can use the writeToFile:atomically:encoding:error: method:
NSString *string = @"Hello, World";
NSString *filePath = [docDirectory stringByAppendingString:@"/File.txt"];
[string writeToFile:filePath
atomically:YES
encoding:NSUTF8StringEncoding
error:nil];
If you want to simply read a plain-text file, you can use the
NSString class method stringWithContentsOfFile:encoding:error:
to read from the file:
NSString *fileContents = [NSString stringWithContentsOfFile:filePath
encoding:NSUTF8StringEncoding error:nil];
NSLog(@"%@", fileContents);
1.2. Creating temporary files
To obtain the path to the default location to store temporary files, you can
use the NSTemporaryDirectory
method:
NSString *tempDir = NSTemporaryDirectory();
1.3. Other file manipulation
The NSFileManager class can
be used for moving, copying, creating, and deleting files.
2. Storing Information in an SQL Database
The public domain SQLite library
is a lightweight transactional database. The library is included in the
iPhone SDK and will probably do most of the heavy lifting you need for
your application to store data. The SQLite engine powers several large
applications on Mac OS X, including the Apple Mail application, and is
extensively used by the latest generation of browsers to support HTML5
database features. Despite the “Lite” name, the library should not be
underestimated.
Interestingly, unlike most SQL database engines, the SQLite engine
makes use of dynamic typing. Most other SQL databases implement static
typing: the column in which a value is stored determines the type of a
value. Using SQLite the column type specifies only the type affinity
(the recommended type) for the data stored in that column. However, any
column may still store data of any type.
Each value stored in an SQLite database has one of the storage
types shown in Table 1.
Table 1. SQLite storage types
Storage type | Description |
---|
NULL | The value is a NULL
value. |
INTEGER | The value is a signed
integer. |
REAL | The value is a floating-point
value. |
TEXT | The value is a text
string. |
BLOB | The value is a blob of data, stored
exactly as it was input. |
2.1. Adding a database to your project
Let’s create a database for the City Guide application. Open the
CityGuide project in Xcode and take a look at the
application delegate implementation where we added four starter cities
to the application’s data model. Each city has three bits of
interesting information associated with it: its name, description, and
an associated image. We need to put this information into a database
table.
Open a Terminal window, and at the command prompt type the code
shown in bold:$ sqlite3 cities.sqlite
This will create a cities database and start SQLite in
interactive mode. At the SQL prompt, we need to create our database
tables to store our information. Type the code shown in bold (sqlite> and ...> are the SQLite command
prompts):
SQLite version 3.4.0
Enter ".help" for instructions
sqlite> CREATE TABLE cities(id INTEGER PRIMARY KEY AUTOINCREMENT,
...> name TEXT, description TEXT, image BLOB);
sqlite> .quit
At this stage, we have an empty database and associated table.
We need to add image data to the table as BLOB (binary large object) data;
the easiest way to do this is to use Mike Chirico’s
eatblob.c
program available from http://souptonuts.sourceforge.net/code/eatblob.c.html.
Warning:
The eatblob.c code will not compile out
of the box on Mac OS X, as it makes use of the getdelim and getline functions. Both of these are
GNU-specific and are not made available by the Mac’s
stdlib library. However, you can download the
necessary source code from http://learningiphoneprogramming.com/.
Once you have downloaded the eatblob.c
source file along with the associated
getdelim.[h,c] and
getline[h,c] source files, you can compile the
eatblob program from the command line:
% gcc -o eatblob * -lsqlite3
So, for each of our four original cities defined inside the app
delegate, we need to run the eatblob code:
% ./eatblob cities.sqlite ./London.jpg "INSERT INTO cities (id, name,
description, image) VALUES (NULL, 'London', 'London is the capital of the
United Kingdom and England.', ?)"
to populate the database file with our “starter cities.”
Warning:
It’s arguable whether including the images inside the database
using a BLOB is a good idea, except for small images. It’s a normal
practice to include images as a file and include only metadata
inside the database itself; for example, the path to the included
image. However, if you want to bundle a single file (with starter
data) into your application, it’s a good trick.
We’re now going to add the cities database to the City Guide
application. However, you might want to make a copy of the City Guide
application before modifying it. Navigate to where you saved the
project and make a copy of the project folder, and then rename it,
perhaps to CityGuideWithDatabase. Then open the
new (duplicate) project inside Xcode and use the Project→Rename tool to rename the project
itself.
After you’ve done this, open the Finder again and navigate to
the directory where you created the cities.sqlite
database file. Open the CityGuide project in
Xcode, then drag and drop it into the Resources folder of the
CityGuide project in Xcode. Remember to check the
box to indicate that Xcode should “Copy items into destination group’s
folder.”
To use the SQLite library, you’ll need to add it to your
project. Double-click on the project icon in the Groups & Files
pane in Xcode and go to the Build tab of the Project Info window. In
the Linking subsection of the tab, double-click on the Other Linker
Flags field and add -lsqlite3 to
the flags using the pop-up window.
2.2. Data persistence for the City Guide application
We’ve now copied our database into our project, so let’s add
some data persistence to the City Guide application.
Since our images are now inside the database, you can delete the
images from the Resources group in the Groups & Files pane in
Xcode. Remember not to delete the
QuestionMark.jpg file because our add city view
controller will need that file.
Warning:
SQLite runs much slower on the iPhone than it does in iPhone
Simulator. Queries that run instantly on the simulator may take
several seconds to run on the iPhone. You need to take this into
account in your testing.
If you’re just going to be querying the database, you can leave
cities.sqlite in place and refer to it via the
application bundle’s resource path. However, files in the bundle are
read-only. If you intend to modify the contents of the database as we
do, your application must copy the database file to the application’s
document folder and modify it from there. One advantage to this
approach is that the contents of this folder are preserved when the
application is updated, and therefore cities that users add to your
database are also preserved across application updates.
We’re going to add two methods to the application delegate
(CityGuideDelegate.m). The first copies the
database we included inside our application bundle to the
application’s Document directory, which allows us to write to it. If
the file already exists in that location, it won’t overwrite it. If
you need to replace the database file for any reason, the easiest way
is to delete your application from the simulator and then redeploy it
using Xcode. Add the following method to
CityGuideDelegate.m:
- (NSString *)copyDatabaseToDocuments {
NSFileManager *fileManager = [NSFileManager defaultManager];
NSArray *paths =
NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,
NSUserDomainMask, YES);
NSString *documentsPath = [paths objectAtIndex:0];
NSString *filePath = [documentsPath
stringByAppendingPathComponent:@"cities.sqlite"];
if ( ![fileManager fileExistsAtPath:filePath] ) {
NSString *bundlePath = [[[NSBundle mainBundle] resourcePath]
stringByAppendingPathComponent:@"cities.sqlite"];
[fileManager copyItemAtPath:bundlePath toPath:filePath error:nil];
}
return filePath;
}
The second method will take the path to the database passed back
by the previous method and populate the cities array. Add this method to
CityGuideDelegate.m:
-(void) readCitiesFromDatabaseWithPath:(NSString *)filePath {
sqlite3 *database;
if(sqlite3_open([filePath UTF8String], &database) == SQLITE_OK) {
const char *sqlStatement = "select * from cities";
sqlite3_stmt *compiledStatement;
if(sqlite3_prepare_v2(database, sqlStatement,
-1, &compiledStatement, NULL) == SQLITE_OK) {
while(sqlite3_step(compiledStatement) == SQLITE_ROW) {
NSString *cityName =
[NSString stringWithUTF8String:(char *)
sqlite3_column_text(compiledStatement, 1)];
NSString *cityDescription =
[NSString stringWithUTF8String:(char *)
sqlite3_column_text(compiledStatement, 2)];
NSData *cityData = [[NSData alloc]
initWithBytes:sqlite3_column_blob(compiledStatement, 3)
length: sqlite3_column_bytes(compiledStatement, 3)];
UIImage *cityImage = [UIImage imageWithData:cityData];
City *newCity = [[City alloc] init];
newCity.cityName = cityName;
newCity.cityDescription = cityDescription;
newCity.cityPicture = (UIImage *)cityImage;
[self.cities addObject:newCity];
[newCity release];
}
}
sqlite3_finalize(compiledStatement);
}
sqlite3_close(database);
}
You’ll also have to declare the methods in
CityGuideDelegate.m’s interface file, so add the
following lines to CityGuideDelegate.h just
before the @end directive:
-(NSString *)copyDatabaseToDocuments;
-(void) readCitiesFromDatabaseWithPath:(NSString *)filePath;
In addition, you need to import the
sqlite3.h header file into the implementation, so
add this line to the top of
CityGuideDelegate.m:
#include <sqlite3.h>
After we add these routines to the delegate, we must modify the
applicationDidFinishLaunching:
method, removing our hardcoded cities and instead populating the
cities array
using our database. Replace the applicationDidFinishLaunching: method in
CityGuideDelegate.m with the following:
- (void)applicationDidFinishLaunching:(UIApplication *)application {
cities = [[NSMutableArray alloc] init];
NSString *filePath = [self copyDatabaseToDocuments];
[self readCitiesFromDatabaseWithPath:filePath];
navController.viewControllers = [NSArray arrayWithObject:viewController];
[window addSubview:navController.view];
[window makeKeyAndVisible];
}
We’ve reached a good point to take a break. Make sure you’ve
saved your changes (⌘-Option-S), and click the Build and Run button on
the Xcode toolbar.
OK, we’ve read in our data in the application delegate. However,
we still don’t save newly created cities; we need to insert the new
cities into the database when the user adds them from the AddCityController view. Add the following
method to the view controller
(AddCityController.m):
-(void) addCityToDatabase:(City *)newCity {
NSArray *paths =
NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,
NSUserDomainMask, YES);
NSString *documentsPath = [paths objectAtIndex:0];
NSString *filePath =
[documentsPath stringByAppendingPathComponent:@"cities.sqlite"];
sqlite3 *database;
if(sqlite3_open([filePath UTF8String], &database) == SQLITE_OK) {
const char *sqlStatement =
"insert into cities (name, description, image) VALUES (?, ?, ?)";
sqlite3_stmt *compiledStatement;
if(sqlite3_prepare_v2(database, sqlStatement,
-1, &compiledStatement, NULL) == SQLITE_OK)
{
sqlite3_bind_text(compiledStatement, 1,
[newCity.cityName UTF8String], -1,
SQLITE_TRANSIENT);
sqlite3_bind_text(compiledStatement, 2,
[newCity.cityDescription UTF8String], -1,
SQLITE_TRANSIENT);
NSData *dataForPicture =
UIImagePNGRepresentation(newCity.cityPicture);
sqlite3_bind_blob(compiledStatement, 3,
[dataForPicture bytes],
[dataForPicture length],
SQLITE_TRANSIENT);
}
if(sqlite3_step(compiledStatement) == SQLITE_DONE) {
sqlite3_finalize(compiledStatement);
}
}
sqlite3_close(database);
}
We also need to import the sqlite3.h header
file; add this line to the top of
AddCityController.m:
#include <sqlite3.h>
Then insert the call into the saveCity: method, directly after the line
where you added the newCity to the
cities array. The added line is
shown in bold:
if ( nameEntry.text.length > 0 ) {
City *newCity = [[City alloc] init];
newCity.cityName = nameEntry.text;
newCity.cityDescription = descriptionEntry.text;
newCity.cityPicture = nil;
[cities addObject:newCity];
[self addCityToDatabase:newCity];
RootController *viewController = delegate.viewController;
[viewController.tableView reloadData];
}
We’re done. Build and deploy the application by clicking the
Build and Run button in the Xcode toolbar. When the application opens,
tap the Edit button and add a new city. Make sure you tap Save, and
leave edit mode.
Then tap the Home button in iPhone Simulator to quit the City
Guide application. Tap the application again to restart it, and you
should see that your new city is still in the list.
Congratulations, the City Guide application can now save its
data.
2.3. Refactoring and rethinking
If we were going to add more functionality to the City Guide
application, we should probably pause at this point and refactor.
There are, of course, other ways we could have built this application,
and you’ve probably already noticed that the database (our data model) is now exposed to the
AddCityViewController class as well
as the CityGuideDelegate class.
First, we’d change things so that the cities array is only accessed through the
accessor methods in the application delegate, and then move all of the
database routines into the delegate and wrap them inside those
accessor methods. This would isolate our data model from our view
controller. We could even do away with the cities array and keep the data model “on
disk” and access it directly from the SQL database rather than
preloading a separate in-memory array.
3. Core Data
Sitting above SQLite, and several other possible low-level data
representations, is Core Data. The Core Data framework is an abstraction layer above the
underlying data representation. Technically, Core Data is an
object-graph management and persistence framework. Essentially, this
means that Core Data organizes your application’s model layer, keeping
track of changes to objects. It allows you to reverse those changes on
demand—for instance, if the user performs an undo command—and then
allows you to serialize (archive) the application’s data model directly
into a persistent store