Blog

Retrieving data on the iPhone (with caching)

Bryn Bellomy's picture

Time for part two of my series on iPhone development basics. Last time, I gave some tips on writing settings to a binary file using Apple's Foundation Library. This time I'll show you how to retrieve those settings -- either from a cached version of the property list or from the filesystem itself. As with the first article, let's dive in head first with some code.

First of all, you'll want to add a static NSDictionary member to your settings data controller class for storing the cached settings.

In your .h file:

NSDictionary *cachedSettings;
@property(nonatomic, retain) NSDictionary *cachedSettings;

In your .m file:

static NSDictionary *cachedSettings;
@synthesize cachedSettings;

Here's the loadSettings function. As you can see, I've implemented it as a class method -- there's really no reason to treat your settings data controller as anything other than a singleton with some class methods. Some argue against using singletons in Objective C. In most cases, I could be persuaded to agree; however, app-wide settings don't really make any sense implemented as anything except as global variables, and singletons are essentially the OOP equivalent of globals. I'd love if someone wanted to discuss this in a comment thread, however.

+ (NSDictionary *)loadSettings
{
	if (cachedSettings == nil) {
		NSArray *dirArray = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,
			NSUserDomainMask,
			YES);
 
		NSString *path = [NSString stringWithFormat:@"%@/settings.bin", [dirArray objectAtIndex:0]];
		NSData *settings = [NSData dataWithContentsOfFile:path];
 
		if (settings != nil) {
			NSDictionary *dict = (NSDictionary *)[NSPropertyListSerialization
				propertyListFromData:settings
				mutabilityOption:0
				format:nil
				errorDescription:nil];
 
			if (dict == nil)
				NSLog(@"Error in loadSettings");
 
			cachedSettings = [dict copy];
 
			return dict;
		}
 
		return nil;
	}
	else {
		return cachedSettings;
	}
}

The outermost if block is the heart of our caching. Essentially, if cachedSettings contains data, loadSettings simply returns that data. Otherwise, it loads data out of the filesystem in a pattern much like the save function. Again, NSData does the file I/O for us. Cocoa is made of magic pixie dust. If the file read goes well, we unserialize the binary data stream into an NSDictionary and return that dictionary.

Next, let's revisit the save function I provided last time -- albeit with a minor modification to accommodate for caching.

+ (BOOL) saveSettings:(NSString *)username password:(NSString *)password
{
	NSArray *dirArray = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,
		NSUserDomainMask,
		YES);
 
	NSString *path = [NSString stringWithFormat:@"%@/settings.bin", [dirArray objectAtIndex:0]];
	NSDictionary *dict = [NSDictionary dictionaryWithObjectsAndKeys:username, @"username", password, @"password", nil];
 
	if (cachedSettings != nil) [cachedSettings release];
	cachedSettings = [dict copy];
 
	NSData *data = [NSPropertyListSerialization dataFromPropertyList:dict format:NSPropertyListBinaryFormat_v1_0 errorDescription:nil];
 
	if (data == nil) {
		NSLog(@"Error in SettingsDataController saveSettings");
		return NO;
	}
 
	NSLog(@"Writing to path: %@", path);
 
	if ([data writeToFile:path options:NSAtomicWrite error:nil] == NO) {
		NSLog(@"writeToFile error");
		return NO;
	}
 
	NSLog(@"writeToFile success");
	return YES;
}

Notice lines 4 and 5 of saveSettings:password:.

if (cachedSettings != nil) [cachedSettings release];
cachedSettings = [dict copy];

All we're doing here is checking if a cached version of the settings NSDictionary exists. If so, we release that memory and then copy in the new version that was saved to disk. By copying in a cached version during the save process, we prevent ever having to read from disk more than once. This is, of course, not much of a performance boost for anything we're doing with our Drupal/iPhone integration project, but it's still a best practice, and it's easy and quick enough to implement that there's no good excuse for omitting it.

You could presumably simply set the cachedSettings pointer to the NSDictionary object, rather than memcpying it over. That would probably align better with best practices. I'm still trying to nail down exactly how to retain and release variables in Objective C, however, and I went with the copy because I was more interested in building out new functionality than debugging BAD_ACCESS errors. Again, I'd love a little discussion in a comment thread below.

Saving data on the iPhone

Bryn Bellomy's picture

Over the past month or two, I've been fortunate enough to be able to put some work into an iPhone framework that interfaces with Drupal sites. In doing so, I've learned a number of things about the various Cocoa frameworks that might be useful to others. As such, I'm going to run a short series of blog posts documenting some of these discoveries.

First up is the subject of data storage -- particularly the storage of simple settings. There are several ways to accomplish this in the Cocoa touch framework. If your settings are fairly complicated and you don't expect your user to alter them very frequently, it's recommended that you hook into the Settings application (the way that Mail and Safari do, for example). Of course, this is cumbersome by virtue of the fact that you have to exit your app and start up Settings in order to change anything. If your user is tweaking settings frequently, this could spell a UI disaster.

Another option is simply to write settings to a file somewhere in your app's sandbox. For our Drupal/iPhone framework, I've adopted this approach. For most of our sites, I don't expect users to be dealing with very many settings -- username and password, of course, and perhaps a few other options tied into the Content Profile module. Therefore, while we can't take advantage of Apple's powerful Settings framework, data storage and retrieval is still very manageable.

One quick caveat that may seem silly at first. When it comes to interface design, do remember that with this approach you'll be using up valuable screen real estate with a "settings" button (and you can't begin to realize just how valuable this is until you start trying to assemble a complex UI). Plan for this well in advance or you'll find yourself redesigning your UI several times, perhaps even with completely different types of navigation controllers (which is a pain to redo). I began our app with a UINavigationBar, then switched to a UIToolbar, and finally recoded everything with a UITabBar when it became clear that the first two approaches were not appropriate. Not a very pleasant experience.

Objective C is still C, and you have all of the stdio functions at your disposal if you so desire. However, Cocoa comes with some great functions to assist you with file management and to keep you from writing to the wrong place in your app's sandbox. Here's a block of code to get us started. It saves a username and password to a binary file.

+ (BOOL) saveSettings:(NSString *)username password:(NSString *)password
{
	NSArray *dirArray = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,
			NSUserDomainMask,
			YES);
 
	NSString *path = [NSString stringWithFormat:@"%@/settings.bin", [dirArray objectAtIndex:0]];
	NSDictionary *dict = [NSDictionary dictionaryWithObjectsAndKeys:username, @"username", password, @"password", nil];	
	NSData *data = [NSPropertyListSerialization dataFromPropertyList:dict format:NSPropertyListBinaryFormat_v1_0 errorDescription:nil];
 
	if (data == nil) {
		NSLog(@"Error in SettingsDataController saveSettings");
		return NO;
	}
 
	NSLog(@"Writing to path: %@", path);
 
	if ([data writeToFile:path options:NSAtomicWrite error:nil] == NO) {
		NSLog(@"writeToFile error");
		return NO;
	}
 
	NSLog(@"writeToFile success");
	return YES;
}

Let's step through the first part of this save function. NSSearchPathForDirectoriesInDomains() returns an array of directories that match the search criteria. In this case, we're asking it to give us the documents directory for the app within the user's domain (rather than the systemwide domain). This is basically where you'll want to store most of your app's data, settings or otherwise. The second line of the function creates the full path to the settings file. In the third line, we're creating an NSDictionary object with the settings (be sure to end your dictionary with nil! Common mistake). The fourth line serializes the NSDictionary into an NSData object. If there are errors, the NSData object will be nil. If you skip a few lines down, you'll notice that the NSData class also provides methods for writing itself into a file. The NSAtomicWrite setting writes the data to a backup file first and then copies it to the specified path, preventing write errors from corrupting your settings. It's highly recommended that you write atomically.

Next time around I'll have some advice on caching settings to prevent unnecessary writes. Future articles will address sharing data between UITextViews and other ViewControllers, .xib files, using Interface Builder vs. building interfaces programmatically, and Drupal XML-RPC. Stick around!

"Coders at Work"

Dan's picture
Tags:

I know the development community at large has been pretty enamored with the recently released book "Coders at Work" by Peter Seibel and I'm pretty sure the tech team at EchoDitto is tired of hearing me go on about it. So now that I've finally finished it, I'll say my piece and move on.

I enjoy how the book progresses through different types of coding styles and rather than trying to find the holy grail, the developers explain what worked and what didn't work for each situation. I gained a lot of insight from the range of philosophies from Jamie Zawinski's "Shipping is the most important feature" to Donald Knuth who invented Literate Programming

Also interesting is to read just how much the industry has changed. Over forty years ago it was reasonable for a developer to know the whole system from the hardware to the OS to the application. Presently that would pretty much be impossible.

My only complaint is that there are a few long tangents about esoteric topics relating to really old computers. Overall though it is a fun read and is highly recommended for developers.

We Still Love You Drupal

Dan's picture

Drupal is now being used to power WhiteHouse.gov. I came across a criticism of the decision.
While Drupal certainly is not without its flaws the criticisms offered simply don't hold.

1) Drupal knows best - First off this simply isn't true. One can easily setup permissions to allow for the use of JavaScript, by default, it prevents regular users from using JavaScript and that is a good thing.

2) Drupal is impenetrable - I think the button labeled "Create Content" is pretty clear. In the first argument the complaint is that I can't put code in, the second argument I CAN put code in. Getting simple pages up for people who aren't web savvy is one thing Drupal does very well.

3) Drupal hates change - The upgrade process for Drupal could certainly use some improvement but I'm sure the White House has competent technical staff on hand who can upgrade without a snag.

4) Drupal is disorganized - In this complaint there is a link to a module that remedies the problem. Problem solved!

5) Drupal is righteous - Again this would be a valid complaint were the White House website run by a volunteer on weekend who had not used Drupal in the past. I'm sure the technical staff at the White House knows what they are doing and it doesn't take 45 minutes to figure out how to upload a photo.

6) Regarding Recovery.gov - "The site originally used Drupal but soon hired a private contractor—at a reported cost of $18 million—to rework the site." This sounds like a strong case FOR using Drupal.

Free Zip Code Database Import

Dan's picture

When you need a zip code database for the US you should get it from here
http://www.free-zipcodes.com/
But you shouldn't spend time trying to install all the required perl (sigh) libs needed to import it into MySQL.
Simply create the table structure

CREATE TABLE zipcodes (
       zipcode INT NOT NULL PRIMARY KEY,
       latitude FLOAT(10,8),
       longitude FLOAT(10,8),
       state VARCHAR(2),
       city VARCHAR(128),
       county VARCHAR(128)
);

Then fire up Sequel Pro. File-> Import. Set "Fields terminated by" to || and uncheck "First line contains fields names."

As a bonus Sequel Pro supports ssh tunneling out of the box so you can do it directly to firewalled DBs.

As always make sure you back up your DB first incase of failure.