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