Persisting CoreData Backed Views with Three20

by Evan Cordell on

Suppose you have an iPhone app that uses the Three20 library as well as CoreData. Then you’re probably using the TTNavigator object to manage the view stack, and for good reason. TTNavigator is a brilliant creation – in addition to removing plenty of boilerplate code from your source, you also gain the ability to persist the program state when it exits simply by setting a flag somewhere in your app delegate.

Using CoreData with any reasonably complex data model makes TTNavigator a little trickier to use, but only just. Consistent with the TTNavigator idiom of URL mapping, models from one view can be passed to another by passing it as a query. The receiving view can then use that as it pleases. (As a simple example, consider a CoreData backed UITableView which passes a selected object to the receiving detail view). All of this can be found in the Three20 documentation.

But Three20’s persistence doesn’t play well with queries. TTNavigator has no way of knowing what the user selected last time, so whatever object is normally passed into a view is suddenly nil, and this more than likely makes that view useless. It would be nice if there were some way to encode the specific object that’s being passed as a query into a url. TTNavigator allows for this in general with Rails-like route mapping, a la “tt://MyApp/MyViewController/(initWithString:)”. For example, tt://MyApp/MyViewController/foobar would call [[MyViewController alloc] initWithString:@“foobar”].

The most obvious approach would be to create a url that includes the NSManagedObjectID of the NSManagedObject to be passed. If we attach that ID (converted to a string) to the end of the url, then we can just parse that back out in the view controller initializer and snag the object itself from the apps' NSManagedObjectContext.

What’s the problem? The string representations of NSManagedObjectIDs are actually URIs, e.g. they look like: x-coredata://EB8922D9-DC06-4256-A21B-DFFD47D7E6DA/MyEntity/p3. So if we put this into a TTNavigator url, we end up with something ugly, like this: “tt://MyApp/MyViewController/x-coredata://EB8922D9-DC06-4256-A21B-DFFD47D7E6DA/MyEntity/p3”. And now TTNavigator is searching through its routes to find this absurdly long URI! This will not work.

But if we change the representation of the NSManagedObjectID’s URI into, say, a Base64 encoded string, then we could pass it quite easily!

To do that we’ll use an Objective-C category to extend the NSString class:
NSString+Base64.h

    #import <Foundation/Foundation.h>
    
    @interface NSString (NSStringBase64)
    + (NSString *)encodeBase64WithString:(NSString *)strData;
    + (NSString *)encodeBase64WithData:(NSData *)objData;
    + (NSString *)decodeBase64ToStringWithString:(NSString *)strData;
    + (NSData *)decodeBase64WithString:(NSString *)strBase64;
    @end

NSString+Base64.m

#import "NSString+Base64.h"

static const char encodingTable[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";

@implementation NSString (NSStringBase64)
static const char _base64EncodingTable[64] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
static const short _base64DecodingTable[256] = {
        -2, -2, -2, -2, -2, -2, -2, -2, -2, -1, -1, -2, -1, -1, -2, -2,
        -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2,
        -1, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, 62, -2, -2, -2, 63,
        52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -2, -2, -2, -2, -2, -2,
        -2,  0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14,
        15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -2, -2, -2, -2, -2,
        -2, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40,
        41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -2, -2, -2, -2, -2,
        -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2,
        -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2,
        -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2,
        -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2,
        -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2,
        -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2,
        -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2,
        -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2
};

+ (NSString *)encodeBase64WithString:(NSString *)strData {
        return [NSString encodeBase64WithData:[strData dataUsingEncoding:NSUTF8StringEncoding]];
}

+ (NSString *)encodeBase64WithData:(NSData *)objData {
        const unsigned char * objRawData = [objData bytes];
        char * objPointer;
        char * strResult;
    
        // Get the Raw Data length and ensure we actually have data
        int intLength = [objData length];
        if (intLength == 0) return nil;
    
        // Setup the String-based Result placeholder and pointer within that placeholder
        strResult = (char *)calloc(((intLength + 2) / 3) * 4, sizeof(char));
        objPointer = strResult;
        
        // Iterate through everything
        while (intLength > 2) { // keep going until we have less than 24 bits
                *objPointer++ = _base64EncodingTable[objRawData[0] >> 2];
                *objPointer++ = _base64EncodingTable[((objRawData[0] & 0x03) << 4) + (objRawData[1] >> 4)];
                *objPointer++ = _base64EncodingTable[((objRawData[1] & 0x0f) << 2) + (objRawData[2] >> 6)];
                *objPointer++ = _base64EncodingTable[objRawData[2] & 0x3f];
                
                // we just handled 3 octets (24 bits) of data
                objRawData += 3;
                intLength -= 3; 
        }
    
        // now deal with the tail end of things
        if (intLength != 0) {
                *objPointer++ = _base64EncodingTable[objRawData[0] >> 2];
                if (intLength > 1) {
                        *objPointer++ = _base64EncodingTable[((objRawData[0] & 0x03) << 4) + (objRawData[1] >> 4)];
                        *objPointer++ = _base64EncodingTable[(objRawData[1] & 0x0f) << 2];
                        *objPointer++ = '=';
                } else {
                        *objPointer++ = _base64EncodingTable[(objRawData[0] & 0x03) << 4];
                        *objPointer++ = '=';
                        *objPointer++ = '=';
                }
        }
    
        // Terminate the string-based result
        *objPointer = '\0';
    
        // Return the results as an NSString object
        return [NSString stringWithCString:strResult encoding:NSASCIIStringEncoding];
}

+ (NSData *)decodeBase64WithString:(NSString *)strBase64 {
        const char * objPointer = [strBase64 cStringUsingEncoding:NSASCIIStringEncoding];
        int intLength = strlen(objPointer);
        int intCurrent;
        int i = 0, j = 0, k;
    
        unsigned char * objResult;
        objResult = calloc(intLength, sizeof(char));
    
        // Run through the whole string, converting as we go
        while ( ((intCurrent = *objPointer++) != '\0') && (intLength-- > 0) ) {
                if (intCurrent == '=') {
                        if (*objPointer != '=' && ((i % 4) == 1)) {// || (intLength > 0)) {
                                // the padding character is invalid at this point -- so this entire string is invalid
                                free(objResult);
                                return nil;
                        }
                        continue;
                }
        
                intCurrent = _base64DecodingTable[intCurrent];
                if (intCurrent == -1) {
                        // we're at a whitespace -- simply skip over
                        continue;
                } else if (intCurrent == -2) {
                        // we're at an invalid character
                        free(objResult);
                        return nil;
                }
        
                switch (i % 4) {
                        case 0:
                                objResult[j] = intCurrent << 2;
                                break;
                
                        case 1:
                                objResult[j++] |= intCurrent >> 4;
                                objResult[j] = (intCurrent & 0x0f) << 4;
                                break;
                
                        case 2:
                                objResult[j++] |= intCurrent >>2;
                                objResult[j] = (intCurrent & 0x03) << 6;
                                break;
                
                        case 3:
                                objResult[j++] |= intCurrent;
                                break;
                }
                i++;
        }
    
        // mop things up if we ended on a boundary
        k = j;
        if (intCurrent == '=') {
                switch (i % 4) {
                        case 1:
                                // Invalid state
                                free(objResult);
                                return nil;
                
                        case 2:
                                k++;
                                // flow through
                        case 3:
                                objResult[k] = 0;
                }
        }
    
        // Cleanup and setup the return NSData
        NSData * objData = [[[NSData alloc] initWithBytes:objResult length:j] autorelease];
        free(objResult);
        return objData;
}
+ (NSString *)decodeBase64ToStringWithString:(NSString *)strData {
    NSData *data = [NSString decodeBase64WithString:strData];
    return [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
}
@end

The above files were adapted from this guy’s code to suit my needs. Note that the class methods return objects that are retained, so if you use this handle the release appropriately.

Now we can construct a TTNavigator-based app that can persist with CoreData by combining all of these pieces. Here’s a basic example of what it might look like.

AppDelegate.h

- (void)applicationDidFinishLaunching:(UIApplication *)application {
//other initialization and three20 stuff
[map from:@"tt://MyApp/MyDetailView/(initWithObjectID:)" toViewController:[MyDetailViewController class]];
}

MyTableViewController.m

//myObject is an instance of a NSManagedObject subclass
NSString *myObjectID = [NSString encodeBase64WithString:[[[myObject objectID] URIRepresentation] absoluteString]];
    [[TTNavigator navigator] openURLAction:[[TTURLAction actionWithURLPath:[NSString stringWithFormat:@"tt://MyApp/MyDetailView/%@", myObjectID]] applyAnimated:animated]];

MyDetailViewController.h

- (id)initWithObjectID:(NSString *) objID {
    if (self == [super init]){ 
        myID = [NSString decodeBase64ToStringWithString:objID];
    }
    return self;
}
- (void)viewDidLoad {
    [super viewDidLoad];
    NSManagedObjectContext *managedObjectContext = [(AppDelegate *)[[UIApplication sharedApplication] delegate] managedObjectContext];
    NSManagedObjectID *theID = [[managedObjectContext persistentStoreCoordinator] managedObjectIDForURIRepresentation:[NSURL URLWithString:myID]];
    myObject = (MyObject *)[managedObjectContext objectWithID:theID];
}

And that’s it! You might wonder why this is useful, since persistence is not a huge issue with iOS4 thanks to multitasking. Aside from just saving state when the user forces an exit, this is especially useful if also using TTLauncher, because you now have a simple way to refer to any custom view in your app, so that your users can have the ability to “bookmark” favorites on the TTLauncherView.

There is one thing to be wary of, and that is that your object could still return nil if you send it a badly formed ID. Matt Gallagher has written an excellent post on how to fix this, and I think the application to this post is fairly obvious so I’ll just link to the other article here.

(Note: This is from my other job with NCPTT and not directly from NewAperio. A similar blog post will likely appear on NCPTT’s website in the near future.)