In the first demonstration, we’ll add the ability to export recipes from Core Data so that they can be shared. We’ll create an NSOperation, which will create its own NSManagedObjectContext and use it to copy the selected recipes into a JSON structure, which can then be used by the application in several ways (uploaded to a server, emailed to a friend, and so on).
To implement this addition to our application, we need to make a few changes to the user interface. We want to add a button to the UINavigationBar that’s a generic action item. When the button is tapped, it will display an action sheet and give the user an option to mail the recipe, as shown here (we can add other options later).
To accomplish this, we first add the button in the storyboard and associate the button with a new method called -action:. Inside the -action: method, we construct a UIActionSheet and present it to the user.
| - (IBAction)action:(id)sender; |
| { |
| UIActionSheet *sheet = [[UIActionSheet alloc] init]; |
| [sheet addButtonWithTitle:@"Mail Recipe"]; |
| [sheet addButtonWithTitle:@"Cancel"]; |
| [sheet setCancelButtonIndex:([sheet numberOfButtons] - 1)]; |
| [sheet setDelegate:self]; |
| [sheet showInView:[self view]]; |
| } |
Once the user makes a choice with the UIActionSHeet, our PPRDetailViewController will get a callback as its delegate. If the user clicked Cancel, we simply return. Otherwise, we drop into a switch statement to handle the choice the user has made. In this example, we have only one choice available.
| - (void)actionSheet:(UIActionSheet*)actionSheet |
| didDismissWithButtonIndex:(NSInteger)buttonIndex |
| { |
| if (buttonIndex == [actionSheet cancelButtonIndex]) return; |
| switch (buttonIndex) { |
| case 0: //Mail Recipe |
| [self mailRecipe]; |
| break; |
| default: |
| NSLog(@"Unknown index: %li", (long)buttonIndex); |
| } |
| } |
When the user taps Mail Recipe, the switch statement calls our -mailRecipe method.
| - (void)mailRecipe |
| { |
| PPRExportOperation *operation = nil; |
| operation = [[PPRExportOperation alloc] initWithRecipe:[self recipeMO]]; |
| [operation setExportBlock:^(NSData *data, NSError *error) { |
| ZAssert(data || !error, @"Error: %@\n%@", [error localizedDescription], |
| [error userInfo]); |
| //Mail the data to a friend |
| }]; |
| |
| [[NSOperationQueue mainQueue] addOperation:operation]; |
| } |
In the -mailRecipe method, we construct a PPRExportOperation instance, passing it the recipe. We then give the PPRExportOperation a completion block. The completion block is issued to handle the results of the export operation. Note that the block receives back both an NSData and an NSError. If the operation was successful, the NSData is populated. If the operation failed for any reason, the NSData is nil, and the NSError is populated. In a production application, we’d want to respond to the error. For now, we have a developer-level logic check to capture the error.
The next step is to build the PPRExportOperation. The goal of the operation is to accept a single recipe and turn it into a JSON structure. When the export is complete, the operation will execute a completion block and pass the resulting data back to the caller. This gives us the following header:
| #import "PPRRecipeMO.h" |
| |
| typedef void (^ExportCompletionBlock)(NSData *jsonData, NSError *error); |
| |
| @interface PPRExportOperation : NSOperation |
| |
| @property (nonatomic, copy) ExportCompletionBlock exportBlock; |
| |
| - (id)initWithRecipe:(PPRRecipeMO*)recipe; |
| |
| @end |
The initializer for our export operation needs to retain only the NSManagedObjectID of the incoming NSManagedObject. Because the -initWithRecipe method is being called on the queue on which the the incoming NSManagedObject was created (the main queue), we can access its methods. Once we’re in our own -main method, we can no longer access it. However, the NSManagedObjectID is thread-safe and can cross the boundary. So, we grab it now and hold onto it in a property. We pass the entire NSManagedObject so that we can also grab a reference to the NSManagedObjectContext without having to explicitly request it.
| - (id)initWithRecipe:(PPRRecipeMO*)recipe; |
| { |
| if (!(self = [super init])) return nil; |
| |
| [self setIncomingRecipeID:[recipe objectID]]; |
| [self setParentContext:[recipe managedObjectContext]]; |
| |
| return self; |
| } |
Once the operation is started, we must perform a couple of startup tasks. First, we need to create the child context.
| - (void)main |
| { |
| NSAssert([self exportBlock] != NULL, @"No completion block set"); |
| NSManagedObjectContext *localMOC = nil; |
| NSUInteger type = NSPrivateQueueConcurrencyType; |
| localMOC = [[NSManagedObjectContext alloc] initWithConcurrencyType:type]; |
| [localMOC setParentContext:[self parentContext]]; |
Now that we have a private NSManagedObjectContext, we need to kick off a -performBlockAndWait: and then retrieve a local copy of the recipe. In this situation, we want to have the operation block until the processing is completed so that the block isn’t destroyed by ending early.
A call to -objectWithID: returns a local copy of the NSManagedObject. Since we know the object already exists in the parent NSManagedObjectContext, it’s safe to assume that there will be no I/O in retrieving this object.
Once we have a local copy of the recipe, we need to turn it into a JSON structure. To do that we must first convert the NSManagedObject into a dictionary. However, we don’t want to just grab the top-level object; we also want the author, the ingredients, and so on. To get all of those, we’ll have to do a bit of recursive work.
| - (NSDictionary*)moToDictionary:(NSManagedObject*)mo |
| { |
| NSMutableDictionary *dict = [NSMutableDictionary dictionary]; |
| if (!mo) return dict; |
| NSEntityDescription *entity = [mo entity]; |
| |
| NSArray *attributeKeys = [[entity attributesByName] allKeys]; |
| NSDictionary *values = [mo dictionaryWithValuesForKeys:attributeKeys]; |
| [dict addEntriesFromDictionary:values]; |
The first part of the conversion involves grabbing all of the attributes (strings, numbers, dates) from the NSManagedObject and placing them into an NSMutableDictionary. By utilizing KVC, we can do this with a small amount of code. We first ask the NSEntityDescription for all of the attribute names, and then we call -dictionaryWithValuesForKeys: on the NSManagedObject. This returns a dictionary with all of the keys and values for the attributes. We then add the resulting dictionary into our NSMutableDictionary.
| NSDictionary *relationships = [entity relationshipsByName]; |
| NSRelationshipDescription *relDesc = nil; |
| for (NSString *key in relationships) { |
| relDesc = [relationships objectForKey:key]; |
| if (![[[relDesc userInfo] valueForKey:ppExportRelationship] boolValue]) { |
| DLog(@"Skipping %@", [relDesc name]); |
| continue; |
| } |
Next, we must deal with the relationships. Just as with the attributes, we can find out the name of each relationship from the NSEntityDescription. By looping over the resulting NSDictionary, we can process each relationship. However, to make sure we aren’t in an infinite recursion, we check each relationship to see whether we should be following it. If we shouldn’t follow it, we simply skip to the next relationship.
| if ([relDesc isToMany]) { |
| NSMutableArray *array = [NSMutableArray array]; |
| for (NSManagedObject *childMO in [mo valueForKey:key]) { |
| [array addObject:[self moToDictionary:childMO]]; |
| } |
| [dict setValue:array forKey:key]; |
| continue; |
| } |
| NSManagedObject *childMO = [mo valueForKey:key]; |
| [dict addEntriesFromDictionary:[self moToDictionary:childMO]]; |
| } |
| return dict; |
| } |
When performing this copy, it’s easy to accidentally copy the entire Core Data repository. Because all our objects are linked via two-way relationships, if we built a recursive method to copy the objects and follow their relationships, we’d end up with a complete duplicate of all the recipes.
To prevent this, we added a check in each relationship copy. Whenever it follows a relationship, it first checks to make sure that the destination entity should be copied. We do this with a key-value pair in the data model. If there’s a key called ppExportRelationship and that key has a value of NO, we skip the relationship. By taking this step, we guarantee that the entity tree is copied in only one direction, as shown in the figure.
From the perspective of the current NSManagedObject, there are two types of relationships: to-one or to-many. If it’s a to-one relationship, we grab the object at the other end of the relationship and turn it into an NSDictionary for inclusion in our NSMutableDictionary.
If the relationship is a to-many, we need to iterate over the resulting set and turn each NSManagedObject into an NSDictionary and include it in a temporary array. Once we’ve iterated over all of the objects in the relationship, we add the entire array to our master NSMutableDictionary using the relationship name as the key.
Once all of the relationships have been processed, we return the NSMutableDictionary to the -main so that we can complete the final step.
| NSDictionary *objectStructure = [self moToDictionary:localRecipe]; |
| data = [NSJSONSerialization dataWithJSONObject:objectStructure |
| options:0 |
| error:&error]; |
| }]; |
| |
| [self exportBlock](data, error); |
| } |
At the end of the -main, we use the NSJSONSerialization class to turn the NSDictionary structure into a JSON structure. Note that the PPRExportOperation doesn’t care whether the serialization is successful. It just passes the result and the potential error off to the completion block. It’s up to the caller to handle any error in the processing.