Jump to content

How to Store iPhone App Data

0
  adfm's Photo
Posted Jun 17 2010 01:26 PM

You have a few different ways to store application data on your iPhone. This excerpt from Alasdair Allan's Learning iPhone Programming will get you up to speed with the most common ways of storing data from your app on an iPhone.


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.

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

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

Creating temporary files

To obtain the path to the default location to store temporary files, you can use the NSTemporaryDirectory method:

NSString *tempDir = NSTemporaryDirectory();

Other file manipulation

The NSFileManager class can be used for moving, copying, creating, and deleting files.

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

Table 8-1. SQLite storage types
Storage typeDescription
NULLThe value is a NULL value.
INTEGERThe value is a signed integer.
REALThe value is a floating-point value.
TEXTThe value is a text string.
BLOBThe value is a blob of data, stored exactly as it was input.

If you’re not familiar with SQL, I recommend you read Learning SQL, Second Edition by Alan Beaulieu (O’Reilly). If you want more information about SQLite specifically, I also recommend SQLite by Chris Newman (Sams).

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.


Note: If you don’t want to create the database for the City Guide application yourself, you can download a prebuilt copy containing the starter cities from this book’s website.

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.so.../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 ProjectRename 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.

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 

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 

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.

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.

Although we could do this refactoring now, we won’t do so in this chapter. However, in your own applications, I suggest that you don’t access SQLite directly. Instead, use Core Data (discussed next) or be sure to move your SQLite calls into the delegate to abstract it from the view controller.

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.

Core Data is an ideal framework for building the model part of an MVC-based application, and if used correctly it is an extremely powerful tool. I’m not going to cover Core Data in this book.

Learning iPhone Programming

Learn more about this topic from Learning iPhone Programming.

Get the hands-on experience you need to program for the iPhone and iPod Touch. With this easy-to-follow guide, you'll build several sample applications by learning how to use Xcode tools, the Objective-C programming language, and the core frameworks. Before you know it, you'll not only have the skills to develop your own apps, you'll know how to sail through the process of submitting apps to the iTunes App Store.

See what you'll learn


Tags:
1 Subscribe


0 Replies