To build a system that imitates the NSUserDefaults, we need to have a single object that manages the parameters table for us. By doing so, we can treat the entire parameters table as if it were a single object with a dynamic number of accessors. However, we don’t want to have to write an accessor every time that we add a parameter; ideally, we want to just call ‑valueForKey: and ‑setValue:forKey: and not worry about the persistence of these values. Lastly, we want to be able to set up some default values.
An important point about the defaults is that they aren’t persisted to disk. If they get persisted, then later versions that change the default would require additional code to check for persisted defaults and reset them. If, however, we don’t persist them, users of newer versions of the application automatically get the newer defaults for free and, more importantly, don’t get their preferences destroyed if they’ve changed the value from its default.
The DocumentPreferences object accomplishes all of these goals.
| @interface DocumentPreferences : NSObject |
| |
| @property (weak) NSPersistentDocument *associatedDocument; |
| @property (weak) NSDictionary *defaults; |
| |
| - (id)initWithDocument:(NSPersistentDocument*)associatedDocument; |
| - (NSArray*)allParameterNames; |
| - (NSDictionary*)allParameters; |
| |
| @end |
Our DocumentPreferences object expects to receive a reference to its NSPersistentDocument upon initialization. From the passed-in reference, our DocumentPreferences will be able to access the underlying NSManagedObjectContext. We could also just incorporate this design directly into a subclass of NSPersistentDocument; however, that can cause the document object to become quite large and difficult to maintain. Therefore, even though a one-to-one relationship exists between NSPersistentDocument objects and DocumentPreferences objects, we keep them separate to reduce code complexity.
The one thing that’s missing from this header file is any way to access the parameters themselves. There are no methods for this access because we’re going to take advantage of KVC. Whenever another piece of code requests a parameter from our DocumentPreferences object, the ‑valueForUndefinedKey: method is called, and that’s where we handle access to the parameters table.
In this method, we receive the name of the value the caller is trying to retrieve.
| - (id)valueForUndefinedKey:(NSString*)key |
| { |
| id parameter = [self findParameter:key]; |
| if (!parameter && [[self defaults] objectForKey:key]) { |
| return [[self defaults] objectForKey:key]; |
| } |
| return [parameter valueForKey:@"value"]; |
| } |
Use this name to retrieve the NSManagedObject via the ‑findParameter: method and return the NSManagedObject object’s value property. If there’s no parameter with the passed-in name, check the defaults NSDictionary to see if there’s a default for it. If no default is set, let the ‑valueForKey: method return nil to the caller.
In the ‑findParameter: method, construct an NSFetchRequest against the parameters table using a compare on the name property to filter it down to a single result.
| - (NSManagedObject*)findParameter:(NSString*)name; |
| { |
| NSManagedObjectContext *moc; |
| NSManagedObject *param; |
| NSError *error = nil; |
| moc = [[self associatedDocument] managedObjectContext]; |
| NSFetchRequest *request = [[NSFetchRequest alloc] init]; |
| [request setEntity:[NSEntityDescription entityForName:@"Parameter" |
| inManagedObjectContext:moc]]; |
| NSPredicate *predicate = nil; |
| predicate = [NSPredicate predicateWithFormat:@"name == %@", name]; |
| [request setPredicate:predicate]; |
| |
| param = [[moc executeFetchRequest:request error:&error] lastObject]; |
| if (error) { |
| DLog(@"Error fetching parameter: %@\n%@", [error localizedDescription], |
| [error userInfo]); |
| return nil; |
| } |
| return param; |
| } |
Assuming there’s no error on the fetch, return the NSManagedObject. In this method, we’re using the ‑lastObject method on the resulting array as a convenience. ‑lastObject automatically checks for an empty array and returns nil if it is empty. This reduces the complexity and gives us the result we want in a single call. If there’s an error accessing the Core Data stack, report the error and return nil. Note that we don’t create a parameter if there isn’t one in this method. We intentionally separate this out so that we aren’t creating potentially empty parameters. This allows us to request a parameter and check if it’s nil without the concern of generating parameters unnecessarily.
In addition to being able to access a parameter, we need to set parameters. This is done in the counterpart method of ‑valueForUndefinedKey: called ‑setValue:forUndefinedKey:. We first notify the system we’re going to be changing the value associated with the passed-in key. This is part of KVO and is required so that notifications work correctly. After starting the KVO notification, attempt to retrieve the NSManagedObject from the parameters table. If there’s no NSManagedObject for the passed-in key, check the defaults NSDictionary to see whether there’s a default. If there’s a default set and the passed-in value matches the default, complete the KVO notification and return. If the default value doesn’t match the passed-in value, create a new NSManagedObject for the passed-in key.
| - (void)setValue:(id)value forUndefinedKey:(NSString*)key |
| { |
| [self willChangeValueForKey:key]; |
| NSManagedObject *parameter = [self findParameter:key]; |
| if (!parameter) { |
| if ([[self defaults] valueForKey:key] && |
| [value isEqualTo:[[self defaults] valueForKey:key]]) { |
| [self didChangeValueForKey:key]; |
| return; |
| } |
| parameter = [self createParameter:key]; |
| } else { |
| if ([[self defaults] valueForKey:key] && |
| [value isEqualTo:[[self defaults] valueForKey:key]]) { |
| [self willChangeValueForKey:key]; |
| NSManagedObjectContext *moc = nil; |
| moc = [[self associatedDocument] managedObjectContext]; |
| [moc deleteObject:parameter]; |
| [self didChangeValueForKey:key]; |
| return; |
| } |
| } |
| if ([value isKindOfClass:[NSNumber class]]) { |
| [parameter setValue:[value stringValue] forKey:@"value"]; |
| } else if ([value isKindOfClass:[NSDate class]]) { |
| [parameter setValue:[value description] forKey:@"value"]; |
| } else { |
| [parameter setValue:value forKey:@"value"]; |
| } |
| [self didChangeValueForKey:key]; |
| } |
If there’s an NSManagedObject and a default set for the key, compare the default and passed-in values. If they match, we delete the NSManagedObject, which resets the parameter to the default. Once we pass the checks against the default and/or create the NSManagedObject, we test the value to see if it’s an NSNumber or NSDate. If it is, pass in its ‑stringValue or ‑description as the value for the NSManagedObject. Otherwise, pass in the value directly to the NSManagedObject. Once the value is set, call ‑didChangeValueForKey: to complete the KVO notification.
The ‑createParameter: method creates a new NSManagedObject and sets the name property with the passed-in value.
| - (NSManagedObject*)createParameter:(NSString*)name |
| { |
| NSManagedObject *param; |
| NSManagedObjectContext *moc; |
| moc = [[self associatedDocument] managedObjectContext]; |
| param = [NSEntityDescription insertNewObjectForEntityForName:@"Parameter" |
| inManagedObjectContext:moc]; |
| [param setValue:name forKey:@"name"]; |
| return param; |
| } |
It doesn’t set the value property, leaving that up to the caller. This allows us to set a nil parameter if we really need one.
We have a couple of useful convenience methods. The first, ‑allParameters, returns an NSDictionary of all the parameters, including the defaults. In this method, we create an NSFetchRequest for the Parameter entity without an NSPredicate.
| - (NSDictionary*)allParameters; |
| { |
| NSManagedObjectContext *moc; |
| NSError *error = nil; |
| moc = [[self associatedDocument] managedObjectContext]; |
| NSFetchRequest *request = [[NSFetchRequest alloc] init]; |
| [request setEntity:[NSEntityDescription entityForName:@"Parameter" |
| inManagedObjectContext:moc]]; |
| NSArray *params = [moc executeFetchRequest:request error:&error]; |
| if (error) { |
| DLog(@"Error fetching parameter: %@\n%@", [error localizedDescription], |
| [error userInfo]); |
| return nil; |
| } |
| NSMutableDictionary *dict = [[self defaults] mutableCopy]; |
| for (NSManagedObject *param in params) { |
| NSString *name = [param valueForKey:@"name"]; |
| NSString *value = [param valueForKey:@"value"]; |
| [dict setValue: value forKey:name]; |
| } |
| return dict; |
| } |
We take the resulting NSArray from the fetch and loop over it. Within that loop, we add each NSManagedObject to an NSMutableDictionary derived from the default NSDictionary. This ensures we have both the default values and the Parameter entries included in the final NSDictionary.
Like ‑allParameters, ‑allParameterNames is a convenience method that returns an NSArray of the keys currently set or defaulted.
| - (NSArray*)allParameterNames; |
| { |
| NSManagedObjectContext *moc; |
| NSError *error = nil; |
| moc = [[self associatedDocument] managedObjectContext]; |
| NSFetchRequest *request = [[NSFetchRequest alloc] init]; |
| [request setEntity:[NSEntityDescription entityForName:@"Parameter" |
| inManagedObjectContext:moc]]; |
| NSArray *params = [moc executeFetchRequest:request error:&error]; |
| if (error) { |
| DLog(@"Error fetching parameter: %@\n%@", [error localizedDescription], |
| [error userInfo]); |
| return nil; |
| } |
| |
| NSMutableArray *keys = [[[self defaults] allKeys] mutableCopy]; |
| for (NSManagedObject *param in params) { |
| NSString *name = [param valueForKey:@"name"]; |
| [keys addObject:name]; |
| } |
| return keys; |
| } |
Just like the ‑allParameters method, it retrieves all the parameter NSManagedObject objects and loops over them. Within that loop, it adds the name property to an NSMutableArray derived from the defaults NSDictionary.