The reverse of exporting recipes is to be able to import them. The experience we’re looking for is as follows:
To accomplish this workflow, we need to step into our UIApplicationDelegate and do a few updates.
| - (BOOL)application:(UIApplication *)application |
| openURL:(NSURL *)url |
| sourceApplication:(NSString *)sourceApplication |
| annotation:(id)annotation |
| { |
| if ([[self dataController] isPersistenceInitialized]) { |
| [self consumeIncomingFileURL:url]; |
| } else { |
| [self setFileToOpenURL:url]; |
| } |
| return YES; |
| } |
The first change adds the method -application: openURL: sourceApplication: annotation:. This method will be called whenever another application is requesting that we open a file. If our application has been running for a while and the Core Data stack is fully initialized, then we can process the file immediately. If the stack isn’t fully initialized (for instance, it freshly launched with the opening of the file), then we can store the NSURL and use it once the context has been initialized.
| - (void)contextInitialized; |
| { |
| //Finish UI initialization |
| if (![self fileToOpenURL]) return; |
| [self consumeIncomingFileURL:[self fileToOpenURL]]; |
| } |
The next change takes place in -contextInitialized. Once the context is fully initialized, we can consume the NSURL that was passed in on the launch. Since we’re going to be consuming the NSURL from more than one potential entry point, we abstract away the handling into a -consumeIncomingFileURL: method. Thus, the -contextInitialized just needs to check whether there is an NSURL to consume and hand it off. Since -consumeIncomingFileURL: returns a pass or fail, we can add a logic check here to help capture failures during the development.
The final change is to handle the consumption of the NSURL. We’ve already defined a method as -consumeIncomingFileURL:.
| - (void)consumeIncomingFileURL:(NSURL*)url; |
| { |
| NSData *data = [NSData dataWithContentsOfURL:url]; |
| PPRImportOperation *op = [[PPRImportOperation alloc] initWithData:data]; |
| [op setMainContext:[[self dataController] managedObjectContext]]; |
| [op setImportBlock:^(BOOL success, NSError *error) { |
| if (success) { |
| //Clear visual feedback |
| } else { |
| //Present an error to the user |
| } |
| }]; |
| [[NSOperationQueue mainQueue] addOperation:op]; |
| |
| //Give visual feedback of the import |
| } |
To consume the NSURL, we first load it into an NSData object. We can then pass the NSData instance off to a newly created PPRImportOperation. Once the operation is complete, we’ll display something graphically to the user and kick off the operation. The completion block for the operation checks to see whether there’s an error and then reports the error or dismisses the graphical status.
Our PPRImportOperation has a number of similarities to the PPRExportOperation. However, there’s also a bit more complexity.
| NSManagedObjectContext *localMOC = nil; |
| NSUInteger type = NSPrivateQueueConcurrencyType; |
| localMOC = [[NSManagedObjectContext alloc] initWithConcurrencyType:type]; |
| [localMOC setParentContext:[self mainContext]]; |
As with our PPRExportOperation, we start off with the -main method. The first thing we want to do in the -main is construct the private NSManagedObjectContext and associate it as a child for the main context.
| [localMOC performBlockAndWait:^{ |
| [self processRecipeIntoContext:localMOC]; |
| }]; |
| } |
Now that we have the private context constructed, we want to kick off the consumption of the JSON payload. We could just put the code directly in the -main method, but that would cause the method to get overly long, which can impact maintainability. Therefore, the -performBlockAndWait: method will call -processRecipeIntoContext: instead and pass in the context. Since the -processRecipeIntoContext: method is being called inside the block, it’s still on the proper queue and there’s no threading violation.
| - (void)processRecipeIntoContext:(NSManagedObjectContext*)moc |
| { |
| NSError *error = nil; |
| id recipesJSON = nil; |
| recipesJSON = [NSJSONSerialization JSONObjectWithData:[self incomingData] |
| options:0 |
| error:&error]; |
| if (!recipesJSON) { |
| [self importBlock](NO, error); |
| return; |
| } |
| |
| NSManagedObject *recipeMO = nil; |
| NSEntityDescription *entity = [NSEntityDescription entityForName:@"Recipe" |
| inManagedObjectContext:moc]; |
| |
| if ([recipesJSON isKindOfClass:[NSDictionary class]]) { |
| recipeMO = [[NSManagedObject alloc] initWithEntity:entity |
| insertIntoManagedObjectContext:moc]; |
| |
| [self populateManagedObject:recipeMO fromDictionary:recipesJSON]; |
| return; |
| } else { |
| NSAssert([recipesJSON isKindOfClass:[NSArray class]], |
| @"Unknown structure root: %@", [recipesJSON class]); |
| for (id recipeDict in recipesJSON) { |
| NSAssert([recipeDict isKindOfClass:[NSDictionary class]], |
| @"Unknown recipe structure: %@", [recipeDict class]); |
| recipeMO = [[NSManagedObject alloc] initWithEntity:entity |
| insertIntoManagedObjectContext:moc]; |
| |
| [self populateManagedObject:recipeMO fromDictionary:recipeDict]; |
| } |
| |
| if (![moc save:&error]) { |
| NSLog(@"Error saving context: %@\n%@", [error localizedDescription], |
| [error userInfo]); |
| abort(); |
| } |
| } |
| } |
We start the -processRecipeIntoContext: method by using the NSJSONSerializer to convert the NSData into a JSON structure. If that conversion fails, we call the completion block and let the operation finish.
Once we have the data in a JSON structure, we must check whether the top-level object is an NSArray or an NSDictionary. Adding this check makes the import operation more flexible and capable of handling the imported recipes. If the top-level object is an NSDictionary, we know there is a single recipe being imported and begin the import operation. If the top-level object is an NSArray, we iterate over the array and construct a recipe for each included dictionary.
For each dictionary (whether there are one or many), we construct an NSManagedObject and pass both the dictionary and the NSManagedObject into -populateManagedObject: fromDictionary:.
| - (void)populateManagedObject:(NSManagedObject*)mo |
| fromDictionary:(NSDictionary*)dict |
| { |
| NSManagedObjectContext *context = [mo managedObjectContext]; |
| NSEntityDescription *entity = [mo entity]; |
| NSArray *attKeys = [[entity attributesByName] allKeys]; |
| NSDictionary *atttributesDict = [dict dictionaryWithValuesForKeys:attKeys]; |
| [mo setValuesForKeysWithDictionary:atttributesDict]; |
In the first part of the -populateManagedObject: fromDictionary:, we want to populate all the attributes of the NSManagedObject. The process is the reverse of what we accomplished in the PPRExportOperation. Here are the steps:
| NSManagedObject* (^createChild)(NSDictionary *childDict, |
| NSEntityDescription *destEntity, |
| NSManagedObjectContext *context); |
| createChild = ^(NSDictionary *childDict, NSEntityDescription *destEntity, |
| NSManagedObjectContext *context) { |
| NSManagedObject *destMO = nil; |
| destMO = [[NSManagedObject alloc] initWithEntity:destEntity |
| insertIntoManagedObjectContext:context]; |
| [self populateManagedObject:destMO fromDictionary:childDict]; |
| return destMO; |
| }; |
The next step is to create a block to avoid repeating ourselves later in this method. When we’re dealing with the creation of relationships, we need to create one or more child objects and associate them with the NSManagedObject we’re currently working on. Whether we’re creating one or many child objects, the steps are virtually identical. We’ll use a block to perform the identical portions of the process and avoid ending up with two copies of that code.
Therefore, the purpose of the block is to construct the new NSManagedObject and recursively call -populateManagedObject: fromDictionary: for the new NSManagedObject. The block makes the rest of this method easier to follow and maintain.
| NSDictionary *relationshipsByName = [entity relationshipsByName]; |
| NSManagedObject *destMO = nil; |
| |
| for (NSString *key in relationshipsByName) { |
| id childStructure = [dict valueForKey:key]; |
| if (!childStructure) continue; //Relationship not populated |
| NSRelationshipDescription *relDesc nil; |
| relDesc = [relationshipsByName valueForKey:key]; |
| NSEntityDescription *destEntity = [relDesc destinationEntity]; |
| |
| if (![relDesc isToMany]) { //ToOne |
| destMO = createChild(childStructure, destEntity, context); |
| [mo setValue:destMO forKey:key]; |
| continue; |
| } |
| |
| NSMutableSet *childSet = [NSMutableSet set]; |
| for (NSDictionary *childDict in childStructure) { |
| destMO = createChild(childDict, destEntity, context); |
| [childSet addObject:destMO]; |
| } |
| [self setValue:childSet forKey:key]; |
| } |
| } |
With the attributes populated, we now need to populate the relationships. We grab the dictionary from the NSEntityDescription that describes all of the relationships of our current NSManagedObject and then iterate over them. For each relationship, we check whether a value is set. If no value has been set, then there’s nothing to process. The iterator returns the key from the dictionary, which is the name of the relationship (and also the name of the accessor).
If there’s a value to process, we check whether the relationship is a to-one or a to-many. If it is a to-many, we can invoke our block and create the child NSManagedObject. We can then set the resulting NSManagedObject using KVC and the key from our relationship dictionary.
If the relationship is a to-many, we kick off an iterator to step over the collection and create one NSManagedObject for each NSDictionary inside the collection.
Once we’ve walked the entire JSON structure, the final step is to save the local NSManagedObjectContext. That save results in the changes being pushed directly to the main NSManagedObjectContext and the user interface is updated accordingly.