Learning Core Data for iOS (2014)

11. Background Processing

Everything must be made as simple as possible, but not simpler.

Albert Einstein

Chapter 10, “Performance,” gave recommendations on how to configure a managed object model for optimal performance. Measuring performance with Instruments was also demonstrated. Full-size photos were removed from the Prepare and Shop table view cells, and yet there are no thumbnails in their place. The process to create thumbnails from photos will be intensive, so it cannot be performed in the foreground. Thumbnail creation aside, even the simple act of saving a context has the potential to impact the user interface if there are many changes to commit. This chapter will use the example of thumbnail generation to demonstrate how to perform an intensive task using a private queue context. In addition, background save will be implemented by introducing a parent context between the persistent store and existing main queue context.

Background Save

Because the excessive memory usage issues were resolved in Chapter 10, the Prepare and Shop table views now display nil thumbnails. The process to create thumbnail images dynamically will be an intensive one, so it should be performed in the background to prevent impact on the user interface. To support the process of thumbnail creation, background save will be implemented first. The easiest way to achieve background save is to use two contexts in a parent and child hierarchy. The background context, referred to as the _parentContext, will be configured to use a private queue by setting its concurrency type to NSPrivateQueueConcurrencyType. The foreground context that underpins the user interface is referred to as _context. It is already configured to use the main queue, because its concurrency type is NSMainQueueConcurrencyType. Both contexts exist in memory, so intercommunication between them is very fast.

A child context has no persistent store. Instead, a parent context acts as the persistent store for its child context. When a child context is saved, changes go to its parent. For those changes to be persisted, a save must then be performed on the parent context, too. As the parent context runs on a private queue, the save will not impact the user interface. Figure 11.1 shows an overview of this process.

Image

Figure 11.1 Background save with parent and child contexts


Note

To continue building the sample application, you’ll need to have added the previous chapter’s code to Grocery Dude. Alternatively, you may download, unzip, and use the project up to this point from http://www.timroadley.com/LearningCoreData/GroceryDude-AfterChapter10.zip. Any time you start using an Xcode project from a ZIP file, it’s good practice to click Product > Clean. This practice ensures there’s no residual cache from previous projects using the same name.


When the context hierarchy is configured, at least one context will be configured on a private queue. Private queue context configuration must be performed using blocks. Use performBlockAndWait when configuring a context on a private queue so that it is ready before it is needed. The_context configuration can be performed outside of a block because it already runs on the main queue. As discussed in Chapter 10, it’s a good idea to configure all contexts with a non-default merge policy to settle conflicts, in case they arise. The updated code involved in configuring a context hierarchy is shown in Listing 11.1 in bold.

Listing 11.1 CoreDataHelper.m: init


- (id)init {
if (debug==1) {
    NSLog(@"Running %@ '%@'", self.class, NSStringFromSelector(_cmd));
}
    self = [super init];
    if (!self) {return nil;}

    _model = [NSManagedObjectModel mergedModelFromBundles:nil];
    _coordinator =
    [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:_model];

    _parentContext = [[NSManagedObjectContext alloc]
                       initWithConcurrencyType:NSPrivateQueueConcurrencyType];
    [_parentContext performBlockAndWait:^{
        [_parentContext setPersistentStoreCoordinator:_coordinator];
        [_parentContext
                      setMergePolicy:NSMergeByPropertyObjectTrumpMergePolicy];
    }];

    _context = [[NSManagedObjectContext alloc]
                             initWithConcurrencyType:NSMainQueueConcurrencyType];
    [_context setParentContext:_parentContext];
    [_context setMergePolicy:NSMergeByPropertyObjectTrumpMergePolicy];

    _importContext = [[NSManagedObjectContext alloc]
                          initWithConcurrencyType:NSPrivateQueueConcurrencyType];
    [_importContext performBlockAndWait:^{
        [_importContext setPersistentStoreCoordinator:_coordinator];
        [_importContext setMergePolicy:NSMergeByPropertyObjectTrumpMergePolicy];
        [_importContext setUndoManager:nil]; // the default on iOS
    }];

    _sourceCoordinator = [[NSPersistentStoreCoordinator alloc]
                                              initWithManagedObjectModel:_model];
    _sourceContext = [[NSManagedObjectContext alloc]
                          initWithConcurrencyType:NSPrivateQueueConcurrencyType];
    [_sourceContext performBlockAndWait:^{
        [_sourceContext setMergePolicy:NSMergeByPropertyObjectTrumpMergePolicy];
        [_sourceContext setPersistentStoreCoordinator:_sourceCoordinator];
        [_sourceContext setUndoManager:nil]; // the default on iOS
    }];
    return self;
}


Update Grocery Dude as follows to implement a parent and child context hierarchy:

1. Add the following code to CoreDataHelper.h before any other properties are declared:

@property (nonatomic, readonly) NSManagedObjectContext       *parentContext;

2. Replace the init method of CoreDataHelper.m with the code from Listing 11.1.

The next step is to implement a new method called backgroundSaveContext. This method will save the child context to the parent context and then the parent context to the persistent store. The save to the parent context will be fast because it is performed within memory. It doesn’t matter if the save from the parent context to the persistent store takes a while because it occurs asynchronously on a private queue. The code involved is similar to the existing saveContext method and is shown in Listing 11.2.

Listing 11.2 CoreDataHelper.m: backgroundSaveContext


- (void)backgroundSaveContext {
if (debug==1) {
    NSLog(@"Running %@ '%@'", self.class, NSStringFromSelector(_cmd));
}
    // First, save the child context in the foreground (fast, all in memory)
    [self saveContext];

    // Then, save the parent context.
    [_parentContext performBlock:^{
        if ([_parentContext hasChanges]) {
            NSError *error = nil;
            if ([_parentContext save:&error]) {
                NSLog(@"_parentContext SAVED changes to persistent store");
            }
            else {
                NSLog(@"_parentContext FAILED to save: %@", error);
                [self showValidationError:error];
            }
        }
        else {
            NSLog(@"_parentContext SKIPPED saving as there are no changes");
        }
    }];
}


Update Grocery Dude as follows to implement background save:

1. Add the code from Listing 11.2 to the bottom of the SAVING section of CoreDataHelper.m.

2. Add the following code to CoreDataHelper.h before @end so that other classes may take advantage of background save:

- (void)backgroundSaveContext;

3. Replace [cdh saveContext]; with [cdh backgroundSaveContext]; in the viewDidDisappear method of ItemVC.m.

4. Replace [[self cdh] saveContext]; with [[self cdh] backgroundSaveContext]; in the applicationDidEnterBackground and applicationWillTerminate methods of AppDelegate.m.

5. Add [cdh backgroundSaveContext]; to the bottom of the clear and clearList methods of PrepareTVC.m.

6. Add the following code to the bottom of the clear method of ShopTVC.m and to the bottom of the didSelectRowAtIndexPath method of ShopTVC.m and PrepareTVC.m:

    CoreDataHelper *cdh = [(AppDelegate *)[[UIApplication
sharedApplication] delegate] cdh];
    [cdh backgroundSaveContext];

7. Add the code from the previous step to the bottom of the commitEditingStyle method of LocationsAtHomeTVC.m, LocationsAtShopTVC.m, PrepareTVC.m, and UnitsTVC.m.

Background save has now been implemented. When an attribute value of an object related to an item changes, the Prepare and Shop table views must be refreshed manually. This is because a fetched results controller does not track changes to objects related to its fetched objects. In this case, the fetched results controllers for PrepareTVC and ShopTVC only track changes to Item objects. To work around this issue, a viewDidDisappear method needs to be added to LocationAtHomeVC.m, LocationAtShopVC.m, and UnitVC.m. As shown in Listing 11.3, this new method will post a notification that something has changed whenever the view in question disappears, which will be used to trigger a refresh.

Listing 11.3 LocationAtHomeVC.m, LocationAtShopVC.m, and UnitVC.m: viewDidDisappear


- (void)viewDidDisappear:(BOOL)animated {
if (debug==1) {
    NSLog(@"Running %@ '%@'", self.class, NSStringFromSelector(_cmd));
}
    CoreDataHelper *cdh =
    [(AppDelegate *)[[UIApplication sharedApplication] delegate] cdh];
    [cdh backgroundSaveContext];
 [[NSNotificationCenter defaultCenter] postNotificationName:@"SomethingChanged"
                                                     object:nil
                                                   userInfo:nil];
}


Update Grocery Dude as follows to configure object change notifications:

1. Add the method from Listing 11.3 to the bottom of the VIEW section of LocationAtHomeVC.m, LocationAtShopVC.m, and UnitVC.m.

The application is now ready to test background save:

1. Delete Grocery Dude from the iOS Simulator and click Product > Clean in Xcode.

2. Run Grocery Dude on the iOS Simulator and wait for the test data import process to finish. You can track progress in the console log, as the table view will remain blank until the import completes.

3. Create a new item by clicking + on the Prepare tab. Give the new item any name and press Done. A save will be triggered when the ItemVC view disappears.

Whenever a save triggers, you should see the main queue _context and private queue _parentContext log a SAVED result to the console, as shown in Figure 11.2.

Image

Figure 11.2 Background save

With background save now implemented, you may be wondering how much benefit it has really added. Saving in the foreground in Grocery Dude was fast to begin with, so the benefits are arguably small. The benefits of background save are more obvious when a larger amount of data needs to be saved. A scenario where background save is beneficial is during an import process.

Background Processing

With the introduction of a parent context, there are implications on the way data is imported. You have already seen how the imported test data was not reflected in the table views during the import. The problem is that the fetched results controller of the Items table view has no idea that the new data is available in the persistent store. Although you could set up notification observers to trigger a merge, there’s no need to when you can instead configure _context as the parent of _importContext. Figure 11.3 illustrates this concept.

Image

Figure 11.3 _context as a parent of _importContext

Update Grocery Dude as follows to configure _importContext with a parent:

1. Remove the following line of code from the init method of CoreDataHelper.m:

[_importContext setPersistentStoreCoordinator:_coordinator];

2. Add the following code in its place:

[_importContext setParentContext:_context];

Although it won’t be used in this chapter, the _sourceContext should also be updated with _context as its parent.

Update Grocery Dude as follows to configure _sourceContext with a parent:

1. Remove the following line of code from the init method of CoreDataHelper.m:

[_sourceContext setPersistentStoreCoordinator:_sourceCoordinator];

2. Add the following code in its place:

[_sourceContext setParentContext:_context];

3. Comment out the following code from the init method of CoreDataHelper.m:

_sourceCoordinator =
[[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:_model];

Now that _importContext is a child of _context, imported data will be immediately visible on table views watching _context after _importContext is saved. This is a benefit of using a fetched results controller–backed table view.

Introducing Faulter

The process of generating thumbnails will impact a potentially large number of objects. Any time a large number of objects are processed, care should be taken to ensure memory bloating and interface lag is avoided. As explained in Chapter 10, you should turn objects into faults once you’re finished with them using refreshObject:mergeChanges:NO after saving the containing context. With a parent and child context hierarchy, you’ll need to do this for every context in the hierarchy. That means for every object imported, a save and fault are required in all three contexts.

The process of saving a context and turning a particular object into a fault is a repeatable one. The code needed to perform this task can be reused regardless of the context involved. As such, a new class called Faulter will be implemented that can be used to turn an object into a fault given itsobjectID. The given context will be saved prior to the object being turned into a fault. Faulter will automatically fault the object through the parent context hierarchy if the given context has a parent. Listing 11.4 shows the header of Faulter.

Listing 11.4 Faulter.h


#import <Foundation/Foundation.h>
#import <CoreData/CoreData.h>

@interface Faulter : NSObject

+ (void)faultObjectWithID:(NSManagedObjectID*)objectID
                inContext:(NSManagedObjectContext*)context;

@end


Update Grocery Dude as follows to add the Faulter class:

1. Select the Generic Core Data Classes group.

2. Click File > New > File....

3. Create a new iOS > Cocoa Touch > Objective-C class and then click Next.

4. Set Subclass of to NSObject and Class name to Faulter and then click Next.

5. Ensure the Grocery Dude target is ticked and then click Create to create the class in the Grocery Dude project directory.

6. Replace all code in Faulter.h with the code from Listing 11.4.

The faultObjectWithID:inContext method of the Faulter implementation will first of all save the given context if there are outstanding changes. If the object to be faulted isn’t already a fault, it is turned into a fault using the refreshObject method of NSManagedObjectContext. Once the object is a fault in the given context, the process is repeated in the parent context, if there is one. The code involved is shown in Listing 11.5.

Listing 11.5 Faulter.m


#import "Faulter.h"
@implementation Faulter

+ (void)faultObjectWithID:(NSManagedObjectID*)objectID
                inContext:(NSManagedObjectContext*)context {

    if (!objectID || !context) {
        return;
    }

    [context performBlockAndWait:^{

        NSManagedObject *object = [context objectWithID:objectID];

        if (object.hasChanges) {
            NSError *error = nil;
            if (![context save:&error]) {
                NSLog(@"ERROR saving: %@", error);
            }
        }

        if (!object.isFault) {

            NSLog(@"Faulting object %@ in context %@", object.objectID, context);
            [context refreshObject:object mergeChanges:NO];
        } else {
            NSLog(@"Skipped faulting an object that is already a fault");
        }

        // Repeat the process if the context has a parent
        if (context.parentContext) {
            [self faultObjectWithID:objectID inContext:context.parentContext];
        }
    }];
}
@end


Update Grocery Dude as follows to implement the Faulter class:

1. Replace all code in Faulter.m with the code from Listing 11.5.

The Faulter class will first be used by the existing importGroceryDudeTestData method of CoreDataHelper.m. The test data import process currently only saves and faults objects in the _importContext. With the new context hierarchy in place, the import process now needs to save and fault new objects in _context and _parentContext, too. Without these additional saves, inserted data would never make its way to the persistent store. Listing 11.6 shows an updated test data import method.

Listing 11.6 CoreDataHelper.m: importGroceryDudeTestData


- (void)importGroceryDudeTestData {
if (debug==1) {
    NSLog(@"Running %@ '%@'", self.class, NSStringFromSelector(_cmd));
}
    NSNumber *imported =
    [[NSUserDefaults standardUserDefaults] objectForKey:@"TestDataImport"];

    if (!imported.boolValue) {
        NSLog(@"Importing test data...");
        [_importContext performBlock:^{

            NSManagedObject *locationAtHome =
           [NSEntityDescription insertNewObjectForEntityForName:@"LocationAtHome"
                                         inManagedObjectContext:_importContext];
            NSManagedObject *locationAtShop =
           [NSEntityDescription insertNewObjectForEntityForName:@"LocationAtShop"
                                         inManagedObjectContext:_importContext];
            [locationAtHome setValue:@"Test Home Location" forKey:@"storedIn"];
            [locationAtShop setValue:@"Test Shop Location" forKey:@"aisle"];

            for (int a = 1; a < 101; a++) {

                @autoreleasepool {

                    // Insert Item
                    NSManagedObject *item =
                    [NSEntityDescription insertNewObjectForEntityForName:@"Item"
                                          inManagedObjectContext:_importContext];
                    [item setValue:[NSString stringWithFormat:@"Test Item %i",a]
                            forKey:@"name"];
                    [item setValue:locationAtHome
                            forKey:@"locationAtHome"];
                    [item setValue:locationAtShop
                            forKey:@"locationAtShop"];

                    // Insert Photo
                    NSManagedObject *photo =
            [NSEntityDescription insertNewObjectForEntityForName:@"Item_Photo"
                                          inManagedObjectContext:_importContext];
                    [photo setValue:UIImagePNGRepresentation(
                                         [UIImage imageNamed:@"GroceryHead.png"])
                             forKey:@"data"];

                    // Relate Item and Photo
                    [item setValue:photo forKey:@"photo"];

                    NSLog(@"Inserting %@", [item valueForKey:@"name"]);
                    [Faulter faultObjectWithID:photo.objectID
                                     inContext:_importContext];
                    [Faulter faultObjectWithID:item.objectID
                                     inContext:_importContext];
                }
            }
            [_importContext reset];

            // ensure import was a one off
            [[NSUserDefaults standardUserDefaults]
                                        setObject:[NSNumber numberWithBool:YES]
                                           forKey:@"TestDataImport"];
            [[NSUserDefaults standardUserDefaults] synchronize];
        }];
    }
    else {
        NSLog(@"Skipped test data import");
    }
}


Update Grocery Dude as follows to enhance the test data import method:

1. Add #import "Faulter.h" to the top of CoreDataHelper.m.

2. Replace the existing importGroceryDudeTestData method in CoreDataHelper.m with the method from Listing 11.6.

3. Delete Grocery Dude from the iOS Simulator and click Product > Clean in Xcode.

4. Run Grocery Dude on the iOS Simulator and wait for the test data import process to finish. You should see items appear immediately as they are imported; however, they still won’t have thumbnails.

The Faulter class will now be used by a new class responsible for creating thumbnails.

Introducing Thumbnailer

Thumbnailer is the name of the new class that will be used to generate photo thumbnails in the background. This class assumes the context hierarchy is configured as per Figure 11.3. To remain generic enough for reuse in other applications, this class takes the following variables:

image entityName is the string name of the entity with a thumbnail attribute.

image thumbnailAttributeName is the string name of the binary data attribute where the proposed thumbnail should be created. This attribute is assumed to exist in the entity specified by entityName.

image photoRelationshipName is the string name of the relationship that starts from the entity specified by entityName and leads to another entity containing the photo data.

image photoAttributeName is the string name of the binary data attribute containing the photo. This attribute is assumed to exist at the destination of the relationship specified in photoRelationshipName.

image sortDescriptors is an optional variable used to sort the array of objects with missing thumbnails. The order of this array determines the order thumbnails are created in. It will look better to the user if thumbnails are generated in the same order as the table view where the thumbnails are displayed.

image importContext should be a context running on a private queue. The thumbnails will be created inside this context.

Listing 11.7 shows the header of the Thumbnailer class.

Listing 11.7 Thumbnailer.h


#import <Foundation/Foundation.h>
#import <CoreData/CoreData.h>

@interface Thumbnailer : NSObject

+ (void)createMissingThumbnailsForEntityName:(NSString*)entityName
                  withThumbnailAttributeName:(NSString*)thumbnailAttributeName
                   withPhotoRelationshipName:(NSString*)photoRelationshipName
                      withPhotoAttributeName:(NSString*)photoAttributeName
                         withSortDescriptors:(NSArray*)sortDescriptors
                           withImportContext:(NSManagedObjectContext*)importContext;
@end


Update Grocery Dude as follows to add the Thumbnailer class:

1. Select the Generic Core Data Classes group.

2. Click File > New > File....

3. Create a new iOS > Cocoa Touch > Objective-C class and then click Next.

4. Set Subclass of to NSObject and Class name to Thumbnailer and then click Next.

5. Ensure the Grocery Dude target is ticked and then click Create to create the class in the Grocery Dude project directory.

6. Replace all code in Thumbnailer.h with the code from Listing 11.7.

The implementation code in Thumbnailer is wrapped in a block. It is expected that the given import context is running on a private background queue. When it is called, the first thing Thumbnailer does is to build an NSFetchRequest using the given variables. This results in an array of pointers to objects with a photo and without a thumbnail. A thumbnail is generated for each of these objects. Once the thumbnail has been created, the now unneeded objects are turned into a fault to save memory. The code involved is shown in Listing 11.8.

Listing 11.8 Thumbnailer.m


#import "Thumbnailer.h"
#import "Faulter.h"

@implementation Thumbnailer
#define debug 1

+ (void)createMissingThumbnailsForEntityName:(NSString*)entityName
                  withThumbnailAttributeName:(NSString*)thumbnailAttributeName
                   withPhotoRelationshipName:(NSString*)photoRelationshipName
                      withPhotoAttributeName:(NSString*)photoAttributeName
                         withSortDescriptors:(NSArray*)sortDescriptors
                       withImportContext:(NSManagedObjectContext*)importContext {
if (debug==1) {
    NSLog(@"Running %@ '%@'", self.class, NSStringFromSelector(_cmd));
}
    [importContext performBlock:^{

        NSFetchRequest *request =
        [NSFetchRequest fetchRequestWithEntityName:entityName];
        request.predicate =
        [NSPredicate predicateWithFormat:@"%K==nil && %K.%K!=nil",
              thumbnailAttributeName, photoRelationshipName, photoAttributeName];
        request.sortDescriptors = sortDescriptors;
        request.fetchBatchSize = 15;
        NSError *error;
        NSArray *missingThumbnails =
        [importContext executeFetchRequest:request error:&error];
        if (error) {NSLog(@"Error: %@", error.localizedDescription);}

        for (NSManagedObject *object in missingThumbnails) {

            NSManagedObject *photoObject =
            [object valueForKey:photoRelationshipName];

            if (![object valueForKey:thumbnailAttributeName] &&
                 [photoObject valueForKey:photoAttributeName]) {

                // Create Thumbnail
                UIImage *photo =
            [UIImage imageWithData:[photoObject valueForKey:photoAttributeName]];
                CGSize size = CGSizeMake(66, 66);
                UIGraphicsBeginImageContextWithOptions(size, NO, 0.0);
                [photo drawInRect:CGRectMake(0, 0, size.width, size.height)];
                UIImage *thumbnail = UIGraphicsGetImageFromCurrentImageContext();
                UIGraphicsEndImageContext();
                [object setValue:UIImagePNGRepresentation(thumbnail)
                          forKey:thumbnailAttributeName];

                // Fault photo object out of memory
                [Faulter faultObjectWithID:photoObject.objectID
                                 inContext:importContext];
                [Faulter faultObjectWithID:object.objectID
                                 inContext:importContext];

                // Remove unused variables
                photo = nil;
                thumbnail = nil;
            }
        }
    }];
}
@end


Update Grocery Dude as follows to implement the Thumbnailer class:

1. Replace all code in Thumbnailer.m with the code from Listing 11.8.

As table views containing items appear, they should use the Thumbnailer class to generate missing thumbnails. This means a viewDidAppear method needs to be added to the PrepareTVC and ShopTVC classes. The call to the class method of Thumbnailer will need to be configured as shown inListing 11.9.

Listing 11.9 PrepareTVC.m and ShopTVC.m: viewDidAppear


- (void)viewDidAppear:(BOOL)animated {
if (debug==1) {
    NSLog(@"Running %@ '%@'", self.class, NSStringFromSelector(_cmd));
}
    [super viewDidAppear:animated];

    // Create missing Thumbnails
    CoreDataHelper *cdh =
    [(AppDelegate *)[[UIApplication sharedApplication] delegate] cdh];
    NSArray *sortDescriptors =
     [NSArray arrayWithObjects:
     [NSSortDescriptor sortDescriptorWithKey:@"locationAtHome.storedIn"
                                   ascending:YES],
     [NSSortDescriptor sortDescriptorWithKey:@"name"
                                   ascending:YES],
                                   nil];

    [Thumbnailer createMissingThumbnailsForEntityName:@"Item"
                           withThumbnailAttributeName:@"thumbnail"
                            withPhotoRelationshipName:@"photo"
                               withPhotoAttributeName:@"data"
                                  withSortDescriptors:sortDescriptors
                                    withImportContext:cdh.importContext];
}


Update Grocery Dude as follows to ensure missing thumbnails are created:

1. Add #import "Thumbnailer.h" to the top of PrepareTVC.m and ShopTVC.m.

2. Add the viewDidAppear method from Listing 11.9 to the top of the VIEW section of PrepareTVC.m and ShopTVC.m.

Run the application again, and you should see thumbnails appear as they’re automatically generated (see Figure 11.4). Scrolling should remain smooth at all times.

Image

Figure 11.4 Photo thumbnails generated in the background

If a photo changes and a thumbnail already exists, it won’t be updated. To resolve this, the associated thumbnail should be set to nil whenever a new photo is taken.

Update Grocery Dude as follows to ensure that new photos have appropriate thumbnails:

1. Update the didFinishPickingMediaWithInfo method of ItemVC.m by inserting item.thumbnail = nil; on the line after item.photo.data is set.

If you run the application on a slow device and navigate to the item view, you may notice slight lag as the item view loads. This is due to the large image being pulled from the store into memory. A performBlock can be used to offload intensive tasks such as this. Listing 11.10 shows in bold additional code used to lazily load a photo on the item view.

Listing 11.10 ItemVC.m: refreshInterface


- (void)refreshInterface {
if (debug==1) {
    NSLog(@"Running %@ '%@'", self.class, NSStringFromSelector(_cmd));
}
    if (self.selectedItemID) {
        CoreDataHelper *cdh =
        [(AppDelegate *)[[UIApplication sharedApplication] delegate] cdh];
        Item *item =
        (Item*)[cdh.context existingObjectWithID:self.selectedItemID error:nil];
        self.nameTextField.text = item.name;
        self.quantityTextField.text = item.quantity.stringValue;
        self.unitPickerTextField.text = item.unit.name;
        self.unitPickerTextField.selectedObjectID = item.unit.objectID;
        self.homeLocationPickerTextField.text = item.locationAtHome.storedIn;
        self.homeLocationPickerTextField.selectedObjectID =
        item.locationAtHome.objectID;
        self.shopLocationPickerTextField.text = item.locationAtShop.aisle;
        self.shopLocationPickerTextField.selectedObjectID =
        item.locationAtShop.objectID;

        [cdh.context performBlock:^{
            self.photoImageView.image =
            [UIImage imageWithData:item.photo.data];
        }];

        [self checkCamera];
    }
}


Update Grocery Dude as follows to lazily load the photo on the item view:

1. Replace the refreshInterface method of ItemVC.m with the one from Listing 11.10.

Run the application again and navigate to the item view. The photo will be loading slightly after the item view interface loads. This likely won’t be noticeable on the iOS Simulator because it is has more resources than a physical iOS device.

Summary

This chapter has shown how to save and import in the background. These techniques ensure you don’t impact the user interface during intensive operations. At the same time, a useful class, Faulter, has been introduced. This class helps reduce the additional code required to save and fault objects through a context hierarchy. You may wish to adopt Faulter in your own projects where a context hierarchy is in play. Likewise, another new class Thumbnailer can be adapted into your own projects to assist in generating thumbnail images.

Exercises

Why not build on what you’ve learned by experimenting?

1. If you have an iOS device, test that the import process and thumbnail creation do not impact the user interface.

2. Update the parser:didStartElement method of CoreDataHelper.m with Faulter support for the context hierarchy. This will allow you to test XML import with multiple contexts. To do this, replace the code in STEP 7 of the parser:didStartElement method of CoreDataHelper.m with the code shown in Listing 11.11.

Listing 11.11 CoreDataHelper.m: parser:didStartElement


[Faulter faultObjectWithID:item.objectID inContext:_importContext];
[Faulter faultObjectWithID:unit.objectID inContext:_importContext];
[Faulter faultObjectWithID:locationAtHome.objectID inContext:_importContext];
[Faulter faultObjectWithID:locationAtShop.objectID inContext:_importContext];


3. Test XML import with Faulter as follows:

image Comment out [self importGroceryDudeTestData]; from the setupCoreData method of CoreDataHelper.m.

image Uncomment [self checkIfDefaultDataNeedsImporting]; from the setupCoreData method of CoreDataHelper.m.

image Replace the alertView:clickedButtonAtIndex method of CoreDataHelper.m with the one shown in Listing 11.12.

Listing 11.12 CoreDataHelper.m: alertView:clickedButtonAtIndex


- (void)alertView:(UIAlertView *)alertView
clickedButtonAtIndex:(NSInteger)buttonIndex {
if (debug==1) {
    NSLog(@"Running %@ '%@'", self.class, NSStringFromSelector(_cmd));
}
    if (alertView == self.importAlertView) {
        if (buttonIndex == 1) { // The 'Import' button on the importAlertView

            NSLog(@"Default Data Import Approved by User");
            // XML Import
            [_importContext performBlock:^{
             [self importFromXML:[[NSBundle mainBundle]
                  URLForResource:@"DefaultData"
                   withExtension:@"xml"]];
             }];
            // Deep Copy Import From Persistent Store
            //[self loadSourceStore];
            //[self deepCopyFromPersistentStore:[self sourceStoreURL]];

        } else {
            NSLog(@"Default Data Import Cancelled by User");
        }
        // Set the data as imported regardless of the user's decision
        [self setDefaultDataAsImportedForStore:_store];
    }
}


image Delete the application and run it again to test the XML import process with Faulter. The console log should show each object as it is faulted through the context hierarchy on the way to the persistent store. The importContext, context, and parentContext all have a different memory address logged to the console (for example, <NSManagedObjectContext: 0x8e12345>).

Prepare for the next chapter by commenting out everything in the setupCoreData method of CoreDataHelper.m except for [self setDefaultDataStoreAsInitialStore]; and [self loadStore];.