Today’s project is another of those oddballs: I’m not really sure it will be interesting to anyone else, and I’m far from confident that I implemented it optimally, but it did solve a real-world problem that cropped up in one of my products. It’s lightweight roll-your-own replacement for NSUserDefaults
. Normally this sort of thing would be superfluous, so let’s begin with the “why” before diving into the “how”.
Motivation
In general, one doesn’t want to have to solve the problem of keeping two distinct data stores synchronized. For instance, if you store a set of records in one file, and an index of those records in a second file, you’re setting yourself up for problems: What happens if one file is saved successfully, but the write to its brother fails? What happens if an index from one pair is matched to a data file from another? This is nasty stuff, and best avoided.
Inside my iKnowPeople app, I keep track of a small quantity of metadata: information about how my Core Data backing store has been configured. This metadata would fit nicely into the NSUserDefaults
system, but for the synchronization issue discussed above: NSUserDefaults
and Core Data use distinct persistent stores, so there’s an insufficient guarantee that data and metadata would move in sync when being backed up, restored, or copied from device to device. It might work, or it might not, and I don’t want to think about it.
My solution was to implement a simple NSUserDefaults
-like module that used Core Data as its backing store; this would guarantee that data and metadata would always move together.
Declarations
Here’s the heart of the PersistentDictionary
declaration:
@interface PersistentDictionary : NSObject
{
NSManagedObjectContext* moc;
NSEntityDescription* entity;
}
- (id)initWithEntityName:(NSString*)name inContext:(NSManagedObjectContext*)managedObjectContext;
- (BOOL)hasValueForKey:(NSString*)key;
- (BOOL)boolForKey:(NSString*)key; // Defaults to NO
- (float)floatForKey:(NSString*)key; // Defaults to 0.0
- (double)doubleForKey:(NSString*)key; // Defaults to 0.0
- (NSInteger)integerForKey:(NSString*)key; // Defaults to 0
- (NSString*)stringForKey:(NSString*)key; // Defaults to @""
- (void)setBool:(BOOL)value forKey:(NSString*)key;
- (void)setFloat:(float)value forKey:(NSString*)key;
- (void)setDouble:(double)value forKey:(NSString*)key;
- (void)setInteger:(NSInteger)value forKey:(NSString*)key;
- (void)setString:(NSString*)value forKey:(NSString*)key;
@end
I also declare 3 extension methods:
@interface PersistentDictionary ()
- (NSArray*)fetchForKey:(NSString*)key;
- (void)deleteForKey:(NSString*)key;
- (void)insertValue:(NSString*)value forKey:(NSString*)key;
@end
Initializer
Most of the definitions involved you can work out for yourself, so I’ll just go over a few highlights. To begin with, the initializer is the only slightly tricky bit:
- (id)initWithEntityName:(NSString*)name inContext:(NSManagedObjectContext*)managedObjectContext
{
if (self = [super init])
{
NSEntityDescription* e = nil;
@try
{
e = [NSEntityDescription entityForName:name inManagedObjectContext:managedObjectContext];
}
@catch (NSException* ne) {}
if (!e)
{
[self release];
return nil;
}
moc = [managedObjectContext retain];
entity = [e retain];
}
return self;
}
Note that this method must be invoked with an Entity
name, and that you must have added an appropriate Entity
to your data model. In this context, “appropriate” refers to an Entity
with (indexed) key
and (non-indexed) value
non-optional String
attributes. If you’d defined an Entity
like this:
Then you would invoke the initializer like this:
metadata = [[PersistentDictionary alloc] initWithEntityName:@"Metadata" inContext:someMOC];
Extension Methods
The PersistentDictionary
uses three “helper” extension methods to do almost all its work:
- (NSArray*)fetchForKey:(NSString*)key
{
// Create a request (for edited Event)
NSFetchRequest* request = [[[NSFetchRequest alloc] init] autorelease];
request.entity = entity;
// Add a predicate to the request
request.predicate = [NSPredicate predicateWithFormat:@"key == %@",key];
// Do a simple fetch (ignore errors)
NSError* err;
return [moc executeFetchRequest:request error:&err];
}
- (void)deleteForKey:(NSString*)key
{
// Perform the delete(s)
for (NSManagedObject* o in [self fetchForKey:key])
{
[moc deleteObject:o];
}
// Commit the change(s)
NSError* err;
if (![moc save:&err])
{
// Handle the error.
NSLog(@"Unresolved error %@, %@", err, [err userInfo]);
}
}
- (void)insertValue:(NSString*)value forKey:(NSString*)key
{
// Can't associate values with nil keys
if (!key) return;
// Create the object
NSManagedObject* o = [[[NSManagedObject alloc] initWithEntity:entity insertIntoManagedObjectContext:moc] autorelease];
// Store the KV pair
[o setValue:key forKey:@"key"];
[o setValue:value forKey:@"value"];
// Commit the changes
NSError* err;
if (![moc save:&err])
{
// Handle the error.
NSLog(@"Unresolved error %@, %@", err, [err userInfo]);
}
}
Gets and Sets
Data retrieval is straight-forward; the only catch is making sure that defaults work properly. This method is typical:
- (BOOL)boolForKey:(NSString*)key
{
NSArray* a = [self fetchForKey:key];
if (![a count])
{
// On an error/nonexistent pair, default to NO
return NO;
}
// In principle, there could be several pairs (this would be a bug); grab the first one arbitrarily
// Note that on non-integer (i.e., buggy) values, this also defaults to 0 (NO)
return [[[a objectAtIndex:0] valueForKey:@"value"] integerValue] == YES;
}
Set methods are even simpler:
- (void)setBool:(BOOL)value forKey:(NSString*)key
{
[self deleteForKey:key];
[self insertValue:[NSString stringWithFormat:@"%d",value] forKey:key];
}
It might have been better to add “change” logic instead of the “delete”/”insert” pair, but I can’t see any practical benefit to it, and it would have been just one more thing to code, test, and possibly go wrong.
Code
You can download the complete header and implementation files, if you’d like to use them in your own projects.