Stack Overflow
Find Me
Tweets
Goodreads

Entries in iOS (3)

Thursday
Feb232012

Persisting Data Using the Plist Format: Part 2/2

This is a follow up to my previous post on persisting PList data to disk as a form of transient storage for data. This second part will just show an easy way to pull that data from disk and get it into memory in an easily usable format.

All of our Plists are stored as dictionaries. The keys for the dictionaries are stored as constants in a code file (Constants.h/.m). This allows for compiler time checking and keeps us from having to litter our code with magic strings when trying to access the values.

Here is a sample workflow to get this method up and running:

The PList Data

Assuming we have a plist that contains the following data:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>firstName</key>
    <string>Mark</string>
</dict>
</plist>

Create a Constants File, and Always Use It

Create a Constants file, and add all keys to it as const NSString. Be strict and only reference these keys when attempting to access data from your NSDictionaries. Don’t shortcut and put the strings directly into your code. Here is an example of how I have this set up:

Constants.h

#import <Foundation/Foundation.h>

@interface Constants : NSObject

extern NSString * const DICT_KEY_FIRST_NAME;

@end

Constants.m

#import "Constants.h"

@implementation Constants

NSString * const DICT_KEY_FIRST_NAME   = @"firstName";

@end

Add your Constants file to your pre-compiled headers file so it is available everywhere

Adding files to your pre-compiled headers makes them available everywhere in your project without having to import each specific class header in the file where you’re trying to use it. Use this convenience judiciously, and only for classes that you truly think you’ll need everywhere. I always include my Constants file in the pre-compiled header to ensure I can access it without having to import it everywhere.

You can find your pre-compiled header file in the Supporting Files group in your project navigator in XCode (if you haven’t moved it). It is typically named {projectname}-Prefix.pch, where {projectname} is the name of your project. At the bottom of this file, you can add #import statements for the header fields you are interested in. Your file should look something like this:

//
// Prefix header for all source files of the 'FeverMonitor' target in the 'FeverMonitor' project
//

#import <Availability.h>

#ifndef __IPHONE_3_0
#warning "This project uses features only available in iOS SDK 3.0 and later."
#endif

#ifdef __OBJC__
    #import <UIKit/UIKit.h>
    #import <Foundation/Foundation.h>
    #import <CoreData/CoreData.h>
    #import "Constants.h"
#endif

The import statement you’re interested in the last one. Make sure you add it inside the #ifdef__OBJC__ block.

This adds an #import statement for each file at compile time which includes these files throughout your entire project. It will also update XCode’s autocomplete indexing so that you are able to use autocomplete for the code on the imported files.

Pull your file from disk, and use the constants defined

Here is an example of how we could wrap this all up and pull some data from a plist file already on disk:

    NSString *filepath = [self convenienceMethodToGetFilePathForPlist];
    NSDictionary *resultsDict = [NSDictionary dictionaryWithContentsOfFile:filepath];
    NSString *firstName = [resultsDict objectForKey:DICT_KEY_FIRST_NAME];

    NSLog(@"First Name: %@",firstName);

Note above that you will need a way to get the plist file path before being able to access it. Also note that I prefix the string constant with DICT_KEY. I do this so that as my constants file grows, I can easily locate the value I’m looking for by typing the prefix I’m looking for.

Well, that wraps up this segment. As a caveat here, I’d like to mention that this data storage methodology, in my opinion, would not scale to larger data sets. You should definitely consider using Core Data for the majority of your data storage scenarios. We decided to use the PList format in this case because the data was transient, and was going to be wiped out on each subsequent change to it.

Thursday
Feb022012

Persisting Simple Data Using the Plist Format

Recently, we had the need to save some simple transient data to disk. For data of any significant size, we would have looked at Core Data for our persistence. In this case, we decided to use the PList format to save data to disk and pull it back off. We went through several ideas to store this data.

Some of the ideas we considered for persistence included:

  • Core Data
  • JSON saved in a flat file format
  • PList saved directly to disk from a Cocoa object such as an NSArray or NSDictionary

Because of our specific set of requirements and our hardware stack, we opted to go with the PList format. Our main driver for this decision was the amount of control we have over the response format from the server. Since we control our servers and their output, we are able to specify PList as a return type. We use the excellent AFNetworking library to perform our networking tasks.

The AFNetworking library has a request named AFPropertyListRequestOperation.* This operation will take an NSURLRequest and return an id that can be cast to a native Cocoa object, such as an NSDictionary or NSArray. Because we know the object’s return structure, we directly cast our object and move on to processing it. From there, it is just a matter of using the writeToFile:Atomically method to persist to disk.

Here is an example workflow:

NSString *urlString = @"http://api.responder?format=plist";
NSURLRequest *urlRequest = [NSURLRequest requestWithURL:[NSURL urlWithString:urlString]];
    AFPropertyListRequestOperation *operation = 
    [AFPropertyListRequestOperation propertyListRequestOperationWithRequest:urlString success:^(NSURLRequest *request, NSHTTPURLResponse *response, id propertyList) {
            // Cache the response
        NSDictionary *responseDict = (NSDictionary *)propertyList;
        NSArray *pathArray = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
        NSString *cachesPath = [pathArray lastObject];
        NSString *filePath = [NSString pathWithComponents:[NSArray arrayWithObjects:cachesPath,@"responseDict.plist",nil]];
        BOOL success = [responseDict writeToFile:filePath atomically:YES];
        if(!success){
            // Handle error
        }        
    } failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error, id propertyList) {
        NSLog(@"API Call failure: %@",[error localizedDescription]);
    }];

One caveat here is to be careful that your dictionary is properly formatted when being returned from the server, and when attempting to save to disk. Initially, we had some bugs when nulls were encountered as a response from the server for specific values. The server parsing logic would simply not generate a value for the key if the value was null. In PList format, this is unacceptable syntax. If you tried to save this to disk, the call would fail and return NO from the writeToFile: method.

For my next post, I’ll do a quick follow up on how to pull the data back off of disk and get it into a usable format in memory.

*Please note that the headers returned from the server must specify application/x-plist data format. The AFPropertyListRequestOperation will check for this in the header and fail the request if it is not present.

Friday
Jun102011

On Discovering New APIs

In working with any programming language, I've always found that if you seem to be fighting things to get work done, you're probably doing it wrong. This is especially true in Cocoa Touch. When i was first learning the frameworks, I read a lot of blogs, books, and any other materials I could get my hands on. ALmost everyone said something to the effect of "You don't get it? Read the docs again."

It took me a while to realize that Apple has excellent documentation. It's gotten a lot better in the Microsoft world, but in the C# 2.0 and 3.5 days, you would frequently go in search of a particular framework method or property, and come across a blank page that was obviously generated by some kind of tool. Or, my favorite was when you had a property that was named something like ToolsArray, and the description would contain verbiage like "The array that holds the tools".

Apple has excellent documentation that gives common sense user cases, detailed definitions, and, in most cases, real world usage examples. I was reading the excellent new book iOS Recipes by Matt Drance and Paul Warren, and discovered a new gem. This one is on the UserDefaults class.

I use the UserDefaults class in a few places in my apps, mostly to hold and maintain state, such as if the app has been launched, if the user has been registered, etc. I always found myself writing guard code like this:


BOOL appWasLoaded = NO;
NSUserDefaults *standardDefaults = [NSUserDefaults standardDefaults];
if([defaults objectForKey:@"appLoadedKey"]{
    appWasLoaded = [defaults boolForKey:@"appLoadedKey"];
}

There is a way to ensure user defaults have some pre-configured settings, which will reduce some of the redundancy in testing that user settings key/value pairs exist. This is accomplished using the following method call on the user defaults:


[[NSUserDefaults standardUserDefaults] registerDefaults:defaults];

When you make this method call, the defaults variable is an NSDictionary that contains they keys and values you want to set as initial defaults. The best part about this particular method, though, is that it will only set the values in user defaults if they don't already exist. What I have been doing to set these initial defaults is to create a .plist file that includes my default values. I pass this into the code as a dictionary, then call the registerDefaults method on startup to provide the default values at runtime. Here is a quick example:

My PList looks like this:

    
    
    
    
        
            Test
            TestValue
        
    

    NSDictionary *defaultValues = 
        [NSDictionary dictionaryWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"defaultValues" 
                                                                                   ofType:@"plist"]];
    [[NSUserDefaults standardUserDefaults] registerDefaults:defaultValues];

After this point, calling

[[NSUserDefaults standardUserDefaults] stringForKey:@"Test"]
will get you the string "TestValue", even if you haven't initialized it somewhere else in your code.

For a full rundown, make sure you check out the official documentation.