Learning Core Data for iOS (2014)

10. Performance

Insanity is doing the same thing, over and over again, but expecting different results.

Albert Einstein

In Chapter 9, “Deep Copy,” techniques were demonstrated that populate a persistent store with data. As persistent stores grow, it’s important to ensure that the application remains responsive. The fetched results controllers in Grocery Dude have already been configured for improved performance using batched fetch requests. What may not be apparent, however, is that the managed object model design plays a key role in producing better performance. This chapter will take you through the process of identifying and eliminating performance issues.

Identifying Performance Issues

As an application nears the end of its development cycle, it’s important to iron out performance issues. Without suitable performance testing, it may not be apparent that an application has a performance issue until it’s too late. The worst-case scenario is that customers use an application for some time and the application slows as their data grows. To prevent this, it’s recommended that you test on the slowest possible device with a data set that’s larger than you would expect any customer to have.

Depending on the nature of an application, performance issues will reveal themselves in different ways. That said, there are some common things to look for in all iOS applications that indicate good or bad performance:

image The application should load quickly.

image Table views should scroll smoothly.

image Views should transition quickly.

image The user interface should remain responsive at all times.

With a large test data set, performance issues should become more obvious. The more obvious performance issues are, the easier it will be to track down the root cause. In addition to a large data set, using large objects such as photos is a common cause of application performance issues. The camera functionality will now be added to Grocery Dude, which opens the door to large objects making their way into the application. Later in this chapter, tips will be given on how the model can be optimized for large objects such as photos.


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-AfterChapter09.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. Before you begin, also remove any existing copies of Grocery Dude from your device to ensure default data is loaded.


Implementing the Camera

When you’re preparing a shopping list, it would be great if you could take a photo of that obscure brand of coffee you love so much. When you’re at the store and can’t remember what it is called, you could then simply refer to the photo. The ability to take a photo will be added to the existing Item view. When an item has a photo, it will be shown both on the Item view and in the table views of the Prepare and Shop tabs. To take a photo, a new button is required on the Item view, and an Image view is needed to display it.

Update Grocery Dude as follows to implement the camera button and Image view:

1. Download and extract add_photo icons from the following URL: http://www.timroadley.com/LearningCoreData/Icons_add_photo.zip.

2. Select the Images.xcassets asset catalog, and then drag the new add_photo icons into it.

3. Select Main.storyboard.

4. Drag an Image View into the existing Scroll View of the Item view.

5. Ensure Attributes Inspector is visible (Option+image+4).

6. Set the Background color of the Image View to Other > Crayons > Mercury (the second lightest gray crayon).

7. Drag a Button into the existing Scroll view of the Item view and configure it as follows using Attributes Inspector (Option+image+4):

image Set Type to Custom.

image Delete the “Button” title text.

image Set Image to add_photo.

8. Ensure both the Height and Width of the new button are 48 using Size Inspector (Option+image+5).

9. Resize the Image View to have an equal width and height and then align it with the new button, as shown in Figure 10.1.

Image

Figure 10.1 Add_photo button and Image view

10. Select the Scroll view of the Item view and click Editor > Resolve Auto Layout Issues > Reset to Suggested Constraints in ItemVC.

The outlets to the new objects must now be connected so they can be referenced in code. The Assistant Editor is used to achieve this.

Update Grocery Dude as follows to connect the camera button and Image view to ItemVC.h:

1. Ensure Main.storyboard is selected.

2. Ensure the Item View Controller is selected.

3. Show the Assistant Editor (Option+image+Return).

4. Set the Assistant Editor to Automatic > ItemVC.h if it isn’t set to this already, as shown at the top of Figure 10.2.

Image

Figure 10.2 Connected camera properties

5. Hold Control and drag a line from the new Image View to the bottom of ItemVC.h before @end. Set the name of the new property to photoImageView. Double-check that Type is UIImageView and that Storage is Strong.

6. Hold Control and drag a line from the camera button to the bottom of ItemVC.h before @end. Set the name of the new property to cameraButton. Double-check that Type is UIButton and that Storage is Strong. Once it’s connected, if you mouse over the circle to the left of thecameraButton property, you should see the camera button highlighted, as shown in Figure 10.2.

7. Show the Standard Editor (image+Return).

So that photos can be taken, the next step is to configure the appropriate protocol and delegate. The ItemVC class will adopt the UIImagePickerControllerDelegate protocol so it receives messages containing the captured photo data. In addition, the ItemVC class will adopt theUINavigationControllerDelegate protocol so it can present the camera interface. Finally, a new property is required to hold the UIImagePickerController instance, known as the camera property.

Update Grocery Dude as follows to configure the ItemVC header for the camera:

1. Add two protocol adoptions (shown in bold) to ItemVC.h as follows:

<UITextFieldDelegate,CoreDataPickerTFDelegate,
UIImagePickerControllerDelegate,UINavigationControllerDelegate>

2. Add the following property to the bottom of ItemVC.h before @end:

@property (strong, nonatomic) UIImagePickerController *camera;

To implement the camera, which in reality is a UIImagePickerController, four new methods are required:

image The checkCamera method ensures the camera is available and toggles the camera button appropriately. This method will be called each time the Item view appears.

image The showCamera method is connected to the cameraButton. When this button is pressed, the camera will show.

image The imagePickerController:didFinishPickingMediaWithInfo method is an image picker delegate method. This delegate method will be called when a photo has been taken. In this method, the photo data will be saved into the photoData attribute value for the currently selected item. The photo is saved as a 640-by-640 JPG image at 50% quality.

image The imagePickerControllerDidCancel method is another image picker delegate method. It is called when Camera view is cancelled. The code in this method will dismiss the Camera view.

Listing 10.1 shows the code involved.

Listing 10.1 ItemVC.m: CAMERA


#pragma mark - CAMERA
- (void)checkCamera {
if (debug==1) {
    NSLog(@"Running %@ '%@'", self.class, NSStringFromSelector(_cmd));
}
    self.cameraButton.enabled =
   [UIImagePickerController
                 isSourceTypeAvailable:UIImagePickerControllerSourceTypeCamera];
}
- (IBAction)showCamera:(id)sender {
if (debug==1) {
    NSLog(@"Running %@ '%@'", self.class, NSStringFromSelector(_cmd));
}
    if ([UIImagePickerController
         isSourceTypeAvailable:UIImagePickerControllerSourceTypeCamera])
    {
        NSLog(@"Camera is available");
        _camera = [[UIImagePickerController alloc] init];
        _camera.sourceType = UIImagePickerControllerSourceTypeCamera;
        _camera.mediaTypes =
        [UIImagePickerController
         availableMediaTypesForSourceType:UIImagePickerControllerSourceTypeCamera];
        _camera.allowsEditing = YES;
        _camera.delegate = self;
        [self.navigationController presentViewController:_camera
                                                animated:YES
                                              completion:nil];
    }
    else
    {
        NSLog(@"Camera not available");
    }
}

- (void)imagePickerController:(UIImagePickerController *)picker
didFinishPickingMediaWithInfo:(NSDictionary *)info {
if (debug==1) {
    NSLog(@"Running %@ '%@'", self.class, NSStringFromSelector(_cmd));
}
    CoreDataHelper *cdh =
    [(AppDelegate *)[[UIApplication sharedApplication] delegate] cdh];

    Item *item =
    (Item*)[cdh.context existingObjectWithID:self.selectedItemID error:nil];

    UIImage *photo =
    (UIImage *)[info objectForKey:UIImagePickerControllerEditedImage];

    NSLog(@"Captured %f x %f photo",photo.size.height, photo.size.width);

    item.photoData = UIImageJPEGRepresentation(photo, 0.5);

    self.photoImageView.image = photo;

    [picker dismissViewControllerAnimated:YES completion:nil];
}
- (void)imagePickerControllerDidCancel:(UIImagePickerController *)picker {
if (debug==1) {
    NSLog(@"Running %@ '%@'", self.class, NSStringFromSelector(_cmd));
}
    [picker dismissViewControllerAnimated:YES completion:nil];
}


Update Grocery Dude to implement the camera:

1. Add the code from Listing 10.1 to the bottom of ItemVC.m before @end.

2. Add the following code to the bottom of the refreshInterface method of ItemVC.m within the if statement:

self.photoImageView.image = [UIImage imageWithData:item.photoData];
[self checkCamera];

3. Select Main.storyboard.

4. Hold down Control and drag a line from the camera button to the yellow circle at the bottom of the Item view; then select Sent Events > showCamera:. If you cannot see the Sent Events > showCamera: option, you may need to save ItemVC.m and try again.

5. Test the new camera functionality on a physical device. You should be able to take photos of items. Note that the camera does not work on the iOS Simulator.

Now that photos can be taken, the table views on the Prepare and Shop tabs will be updated to display photos.

Update Grocery Dude as follows to prepare the table view cells to display photos:

1. Ensure Main.storyboard is selected.

2. Select the Prototype Cell in the table view titled Grocery Dude.

3. Set the Style of the Prototype Cell to Right Detail using Attributes Inspector (Option+image+4).

4. Delete the text “Detail” from within the Prototype Cell.

5. Select the Prototype Cell in the table view titled Items.

6. Set the Style of the Prototype Cell to Right Detail.

7. Delete the text “Detail” from within the Prototype Cell.

8. Add the following code to the bottom of the cellForRowAtIndexPath methods of PrepareTVC.m and ShopTVC.m before return cell;:

cell.imageView.image = [UIImage imageWithData:item.photoData];

Run the application again. Photos you’ve taken should now appear in the table views. Figure 10.3 shows the expected results.

Image

Figure 10.3 The ‘Prepare’ table view with smaller font and photo support

Generating Test Data

Creating a large test data set is easy with some for loops and a little patience. If you need more data, just keep looping through object creation until you have enough for your needs. Techniques to generate test data were touched on early in the book and will be used again, this time with the inclusion of a large image. A copy of the image will intentionally be used separately for each test item. This will simulate the effect of having a unique photo for each test item. The test image will be 2000×2000, which is big enough to emulate a sizable photo taken from the camera. Listing 10.2 shows the code involved in generating test data. Note that for demonstration purposes this method has no reliance on NSManagedObject subclass files. Instead, it uses key-value coding that is Grocery Dude model specific.

Listing 10.2 CoreDataHelper.m: TEST DATA IMPORT


#pragma mark – TEST DATA IMPORT (This code is Grocery Dude data specific)
- (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 {

                    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"];
                    [item setValue:UIImagePNGRepresentation(
                       [UIImage imageNamed:@"GroceryHead.png"])
                            forKey:@"photoData"];
                    NSLog(@"Inserting %@", [item valueForKey:@"name"]);
                    [CoreDataImporter saveContext:_importContext];
                    [_importContext refreshObject:item mergeChanges:NO];
                }
            }
            // force table view refresh
            [self somethingChanged];

            // 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 add test data import functionality:

1. Add the code from Listing 10.2 to CoreDataHelper.m after the existing DATA IMPORT section.

2. Download, extract, and add the following large image to Images.xcassets: http://www.timroadley.com/LearningCoreData/GroceryHead.zip.

The importGroceryDudeTestData method will be called from the setupCoreData method and the existing import methods commented out. The updated code is shown in Listing 10.3.

Listing 10.3 CoreDataHelper.m: setupCoreData


- (void)setupCoreData {
if (debug==1) {
    NSLog(@"Running %@ '%@'", self.class, NSStringFromSelector(_cmd));
}
    //[self setDefaultDataStoreAsInitialStore];
    [self loadStore];
    [self importGroceryDudeTestData];
    //[self checkIfDefaultDataNeedsImporting];
}


If the user modifies items during the import process, there is the potential that the same object could be modified in two contexts at the same time. When one of those contexts is then saved, a merge conflict could result. To handle merge conflicts, a merge policy will be configured in advance. The merge policy decides who wins when these conflicts arise. There are five options:

image NSErrorMergePolicy is the default policy. Merge conflicts prevent the context from being saved.

image NSMergeByPropertyObjectTrumpMergePolicy resolves merge conflicts using object property values from the context to overwrite those in the persistent store.

image NSMergeByPropertyStoreTrumpMergePolicy resolves merge conflicts using object property values from the persistent store to overwrite those in the context.

image NSOverwriteMergePolicy resolves merge conflicts using entire objects from the context to overwrite those in the persistent store.

image NSRollbackMergePolicy resolves merge conflicts using entire objects from the persistent store to overwrite those in the context.

The merge policies boil down to a decision on whether the context or persistent store should win, at either an object or property level.

Update Grocery Dude as follows to enable a merge policy and trigger a test data import:

1. Update the setupCoreData method in CoreDataHelper.m to match the code from Listing 10.3.

2. Replace the init method in CoreDataHelper.m with the one shown in Listing 10.4. The updated code introduces merge policies to each context and is shown in bold.

Listing 10.4 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];
    _context = [[NSManagedObjectContext alloc]
                              initWithConcurrencyType:NSMainQueueConcurrencyType];
    [_context setPersistentStoreCoordinator:_coordinator];
    [_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;
}


Delete Grocery Dude from the iOS Simulator and run it to install it again. The expected result once the import has completed is shown in Figure 10.4.

Image

Figure 10.4 Test data

Once the test data has been imported, try scrolling through the items. You should notice scrolling isn’t smooth, unless you have a fast Mac. If you were to run Grocery Dude on a device, you would receive memory warnings and most likely experience a crash.

Measuring Performance with SQLDebug

Assuming the underlying persistent store is SQLite, one technique for troubleshooting Core Data performance is to use SQLDebug. You can use SQLDebug to gain visibility of how long the automatically generated SQL queries are taking to execute. Debug level 1 is enough to show query execution time. As discussed in Chapter 2, “Managed Object Model Basics,” SQLDebug is enabled by editing the scheme of the application.

Update Grocery Dude as follows to enable SQL Debug level 1:

1. Click Product > Scheme > Edit Scheme....

2. Ensure Run Grocery Dude... and the Arguments tab are selected.

3. Add a new argument by clicking + in the Arguments Passed On Launch section.

4. Enter -com.apple.CoreData.SQLDebug 1 as a new argument and then click OK.

5. Run Grocery Dude on the iOS Simulator again and examine the console log. The expected result is shown in Figure 10.5.

Image

Figure 10.5 Measuring query execution time

Figure 10.5 shows the automatically generated SQL query that fetches items for the prepare table view. Although 100 test items exist in the persistent store, only 50 rows have been retrieved due to setFetchBatchSize:50 configured in the configureFetch method of PrepareTVC.m. Even if there were thousands of items in the persistent store, this code would still protect the application from loading them all into memory at once. A good rule of thumb when choosing a batch fetch size is to think about how many objects you need displayed at the same time. A table view using the default row height on a 4-inch Retina display shows around 10 rows at once. This means a fetch batch size of 50 is excessive. If only 10 rows are visible at once, there’s no point having 40 extra rows loaded and wasting resources. The batch size should be just slightly bigger than the amount of rows that need to be visible at once, so 15 is a more conservative batch size for standard table views. For the picker views, a batch size of around the same amount is good enough.

Update Grocery Dude as follows to alter the fetch request batch sizes:

1. Search the Xcode project for setFetchBatchSize:50.

2. Replace setFetchBatchSize:50 with setFetchBatchSize:15 for every class in the project. If prompted, don’t worry about enabling snapshots.

3. Run Grocery Dude on the iOS Simulator and examine the console log again. The expected result is shown in Figure 10.6.

Image

Figure 10.6 Improved query execution time

The query execution time is faster because fewer rows are fetched. Although setting an appropriate batch size is a good starting point, the scrolling still isn’t smooth, so more investigation is required.

Measuring Performance with Instruments

To measure an application’s performance, profile it with Instruments. Unfortunately, the option to profile Core Data is not available for physical iOS devices. This means you must profile Core Data on the iOS Simulator instead. Although this is not ideal, it still provides good insight into key Core Data–specific metrics such as fetches, cache misses, and saves.

To profile an application using Instruments, you simply run the application in a different way. Instead of just pressing the Run button, press and hold the Run button to select Profile instead. Try this now and notice how the button changes as Instruments loads. The expected result is shown inFigure 10.7.

Image

Figure 10.7 Profiling an application

Once Instruments loads, you’ll be prompted to select a Trace Template, as shown in Figure 10.8. Core Data is found in the File System category.

Image

Figure 10.8 Profiling an application using the Core Data template

Select the Core Data template and click Profile. This will start recording the performance profile of Grocery Dude. It is recommended that Time Profiler and Allocations be used in conjunction with the Core Data template, so stop recording so they can be added.

Configure Instruments as follows to add Time Profiler and Allocations:

1. Press image+L to make the Library visible if it isn’t already.

2. Drag Time Profiler and Allocations from the Library onto the main Instruments window.

3. (Optional) Save this template by clicking File > Save As Template... and then fill in the appropriate information. This will ensure you don’t have to repeat this setup each time you begin profiling.

4. Press Record to begin profiling again and test the scrolling speed of the Prepare table view. Figure 10.9 shows the expected result.

Image

Figure 10.9 Profiling Grocery Dude

As the application launches, you should see a flurry of activity in Time Profiler. You should also see the Allocations increase as the table view scrolls, with Core Data fetches lining up with these increases. If you click either Time Profiler or Allocations and use the Call Tree options, shown inFigure 10.10, you’ll reveal the methods responsible for the heaviest resource utilization.

Image

Figure 10.10 Grocery Dude resource utilization

Clearly the cellForRowAtIndexPath method of PrepareTVC is responsible for the majority of the memory footprint. This is no surprise because the table view cell was configured to display a test image, which happens to be 2000×2000. To see how long the fetches took, select Core Data Fetches and examine the Event List, as shown in Figure 10.11.

Image

Figure 10.11 Measuring fetch duration

Fetch durations are displayed in microseconds, and depending on the type of Mac you’re using, your fetch duration may vary from the screenshot. If you need to know what class and method made these calls, click Event List and change to Call Tree. Of course, you’ll want to set the appropriate options to show only Obj-C and hide system libraries. The same technique applies for viewing Core Data saves and Core Data cache misses. A cache miss is a trip to the persistent store, required because an object was not already in memory.

Improving Performance

A reduced fetched results size helps improve performance. Beyond setFetchBatchSize, there are other NSFetchRequest options you can use to minimize the result set. Not all options are appropriate to all situations:

image Use setFetchLimit and setFetchOffset to reduce fetch results to a specific number of rows starting from a specific point. This may, for example, be appropriate for an application using a Page View Controller to display information. Each page could have a different fetch offset and possibly the same fetch limit.

image Use setPredicate to reduce the result set according to the supplied predicate. If you’re supplying a compound predicate (that is, two or more things to filter by), ensure the predicate that will cut out the most results is put first. Performance gains can also be achieved if a predicate filters against numerical values before textual values. For example, someNumber > 0 && someText LIKE 'whatever' is more efficient than someText LIKE 'whatever' && someNumber > 0.

image Use setPropertiesToGroupBy and setResultType together to emulate the GROUP BY operator otherwise available with raw SQL. When used in conjunction with setHavingPredicate, you can restrict results even further. As an example, you could use this option to produce a count of items in a particular home location.

image Use setIncludesPropertyValues:NO to force the fetch request to exclude property values in cases where you’re only interested in fetching the objectID for each object.

image Use setRelationshipKeyPathsForPrefetching to indicate what relationships should be pre-fetched. Pre-fetching a relationship brings related objects into the context to avoid the inefficiency of fault firing. Use this option when you know you’ll need to access those related objects soon.

None of the additional fetch request options are appropriate for Grocery Dude, and yet the performance is still bad. Once a fetch request has been fully optimized, the performance optimization does not stop there. Obviously showing such a large image on a table view is the main contributor to the performance issues given that cellForRowAtIndexPath is reported to be the busiest method. Perhaps using a pre-scaled thumbnail image would be a better idea. Although thumbnail generation won’t be added until the next chapter, the groundwork can be laid now.

Update Grocery Dude as follows to implement thumbnails:

1. Select Model.xcdatamodeld.

2. Click Editor > Add Model Version....

3. Click Finish to accept the default model name of Model 7.

4. Ensure Model 7.xcdatamodel is selected.

5. Add a Binary Data attribute called thumbnail to the Item entity.

6. Select Model.xcdatamodeld and set the Current Model Version to Model 7 using File Inspector (Option+image+1).

7. Regenerate the NSManagedObject subclass files for the Item entity, overwriting the existing files by clicking Editor > Create NSManagedObject Subclass... and following the prompts. Ensure the Grocery Dude target is ticked before clicking Create.

8. Update the cellForRowAtIndexPath method in PrepareTVC.m and ShopTVC.m to set the row image to display a thumbnail instead of the full photoData, as follows:

cell.imageView.image = [UIImage imageWithData:item.thumbnail];

Profile Grocery Dude as you scroll through the prepare table view again. No images are displayed because there’s no data in the thumbnail attribute yet. Despite the lack of images, the memory footprint is still very large. Figure 10.12 shows the expected result.

Image

Figure 10.12 A large memory footprint

The key point here is that when an object is fetched from the persistent store, all of its attributes are fetched into memory, including the large image. To work around this model constraint, any attribute that is expected to hold a large value should be moved to another entity. A relationship can then be created to that entity so the data is still accessible via the original entity, as required. This faulting technique ensures that the large object is only loaded into memory when it is needed. Once you’re finished with objects, remember to turn them back into a fault by calling refreshObjectwith the mergeChanges:NO option. If changes have been made to the object, you should save it before turning it into a fault.

In addition to faulting, it is recommended that large objects be allowed to reside outside the persistent store. An Allows External Storage option is available for Binary Data attributes underpinned by an SQLite persistent store. This option ensures objects larger than ~1MB are automatically stored outside the SQLite database. This approach is a more efficient way to store large objects. Figure 10.13 shows an example of large objects stored externally to the sqlite file within the application sandbox. Assuming an application with this setting has been run on a device already, the sandbox is viewable in Xcode by clicking Window > Organizer and selecting Applications of a connected device.

Image

Figure 10.13 Allows External Storage

To apply the improved model design principles to Grocery Dude, the photoData attribute will be migrated to a new Item_Photo entity and renamed to data. The Item_Photo entity will be accessible via a new To-One relationship called photo. The inverse To-One relationship will be calleditem. What was once accessed via photoData will now be accessed via photo.data.

Update Grocery Dude as follows to migrate photoData:

1. Ensure Model.xcdatamodeld is selected.

2. Click Editor > Add Model Version....

3. Click Finish to accept the default model name of Model 8.

4. Ensure Model 8.xcdatamodel is selected.

5. Delete the photoData attribute from the Item entity.

6. Create a new entity called Item_Photo.

7. Create a new Binary Data attribute called data in the Item_Photo entity.

8. Configure the data attribute to enable the Allows External Storage option using Data Model Inspector (Option+image+3).

9. Create a To-One relationship from the Item entity to the Item_Photo entity called photo. Also, set the inverse relationship name to item. It’s easiest to change the editor style to Graph in order to create the relationship. To create a relationship, just hold Control and drag a line from one entity to another.

10. Set the Delete Rule of the photo relationship to Cascade using Data Model Inspector (Option+image+3). This will ensure that when an item is deleted, its photo data is deleted, too.

11. Update the selectedUniqueAttributes method of CoreDataHelper.m to cater to the new Item_Photo entity by adding the following line of code on the line before the dictionary is created:

[entities addObject:@"Item_Photo"];[attributes addObject:@"data"];

The resulting data model is shown in Figure 10.14.

Image

Figure 10.14 Grocery Dude Model 8

This type of model change cannot be completely inferred by Core Data unless you don’t mind losing the existing values of photoData. In reality, you would not want to lose existing customer photos, so a model-mapping file is required. Whenever automatic store migration with an inferred mapping is triggered, Core Data checks for a model-mapping file prior to guessing what maps to where. The model-mapping file will need to do the following:

image Map the old photoData attribute from the Item entity to the new data attribute in the Item_Photo entity.

image Map the new photo relationship from the Item entity to the Item_Photo entity.

image Map the new item relationship from the Item_Photo entity to the Item entity.

Although it may have been obvious that the photoData attribute needed to be mapped to the data attribute, it is less obvious that a mapping is required for the new photo and item relationships. Without this relationship mapping, existing photos from photoData will not be correctly related to the new data attribute. This mapping will be created using a Value Expression similar to those inferred for other relationships in the model-mapping file.

Update Grocery Dude as follows to manually map Model 7 to Model 8:

1. Select the Data Model group and then click File > New > File....

2. Click iOS > Core Data > Mapping Model > Next.

3. Select Model 7.xcdatamodel as the source model and click Next.

4. Select Model 8.xcdatamodel as the target model and click Next.

5. Save the mapping model as Model7toModel8, ensure the Grocery Dude target is selected, and then click Create.

6. Select the Item_Photo entity mapping within Model7toModel8.xcmappingmodel.

7. Change the Source of the Item_Photo entity mapping to the Item using the Mapping Model Inspector (Option+image+3). This will automatically rename the entity mapping to ItemToItem_Photo.

8. Set the Value Expression of the Destination Attribute called data to $source.photoData, which ensures objects from the old photoData attribute are migrated to the new data attribute.

9. Likewise, set the Value Expression of the Destination Relationship called item to the following:

FUNCTION($manager, "destinationInstancesForEntityMappingNamed:sourceInstances:"
,"ItemToItem", $source)

10. Select the ItemToItem entity mapping.

11. Set the Value Expression of the Destination Relationship called photo to the following:

FUNCTION($manager, "destinationInstancesForEntityMappingNamed:sourceInstances:"
,"ItemToItem_Photo", $source)

12. Select Model.xcdatamodeld and set the Current Model Version to Model 8 using File Inspector (Option+image+1).

13. Regenerate the NSManagedObject subclass files for all entities in Model 8, overwriting the existing files by clicking Editor > Create NSManagedObject Subclass... and following the prompts. Ensure the Grocery Dude target is ticked before clicking Create.

14. Repeat step 13 to ensure relationships are generated correctly within the NSManagedObject subclass files.

Once the model is upgraded, it is important to remember that the existing sourceStore used to populate default data will become incompatible with the data model. If you tried to load the existing sourceStore, the application would crash. For your convenience, a pre-upgraded source store has been created to include with the project bundle. In reality, you would have had to generate this source store again by first importing default data from Model 6 and then upgrading it to Model 7 and then Model 8. Attempt to run the application again, and you will be prompted to resolve references to the now non-existent photoData attribute.

Update Grocery Dude as follows to use the new attributes and an upgraded default store:

1. Add #import "Item_Photo.h" to the top of ItemVC.m.

2. Update the refreshInterface method of ItemVC.m to set the item image view as follows:

self.photoImageView.image = [UIImage imageWithData:item.photo.data];

3. Update the didFinishPickingMediaWithInfo method of ItemVC.m to store captured photos using the new data attribute. The following code should replace the line of code that Xcode will be reporting as an issue.

if (!item.photo) { // Create photo object it doesn't exist
    Item_Photo *newPhoto =
    [NSEntityDescription insertNewObjectForEntityForName:@"Item_Photo"
                                  inManagedObjectContext:cdh.context];
    [cdh.context obtainPermanentIDsForObjects:
                     [NSArray arrayWithObject:newPhoto] error:nil];
  item.photo = newPhoto;
}
item.photo.data = UIImageJPEGRepresentation(photo, 0.5);

4. Download the persistent store from the following URL and drag it into Grocery Dude: http://www.timroadley.com/LearningCoreData/Model_8_DefaultData.zip. Ensure Copy items into destination group’s folder and the Grocery Dude target is ticked. This persistent store is the Model 8 version of DefaultData.sqlite and should replace the existing file.

Profile Grocery Dude again and scroll through the prepare table view. Compare the results shown previously in Figure 10.12 with those shown in Figure 10.15.

Image

Figure 10.15 A minimized memory footprint

You should notice a significant difference in the memory usage, as ~20MB used by the cellForRowAtIndexPath method has been reduced to ~263KB. By your moving the large objects to the Item_Photo entity, the photo data remains a fault. This means the photo data is not drawn into memory whenever Item objects are fetched.

If you haven’t run the application on the device since upgrading the model, the importGroceryDudeTestData method of CoreDataHelper.m will cause a crash because it is configured for the old model. Listing 10.5 shows an updated method that supports Model 8.

Listing 10.5 CoreDataHelper.m: importGroceryDudeTestData


#pragma mark – TEST DATA IMPORT (This code is Grocery Dude data specific)
- (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"]);
                    [CoreDataImporter saveContext:_importContext];
                    [_importContext refreshObject:item mergeChanges:NO];
                    [_importContext refreshObject:photo mergeChanges:NO];
                }
            }
            // force table view refresh
            [self somethingChanged];

            // 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 enable Model 8 support in the test import method:

1. Replace the importGroceryDudeTestData method in CoreDataHelper.m with the method from Listing 10.5.

Clean Up

When you’re finished with managed objects, it’s important to free up memory by removing them from the context. You can achieve this by turning them into a fault. Whenever the item view disappears, it’s a good opportunity to turn the previously selected item and photo into a fault.

Update Grocery Dude as follows to turn previously selected items into a fault:

1. Add the code shown in Listing 10.6 to the bottom of the viewDidDisappear method of ItemVC.m.

Listing 10.6 ItemVC.m: viewDidDisappear


// Turn item & item photo into a fault
NSError *error;
Item *item =
(Item*)[cdh.context existingObjectWithID:self.selectedItemID error:&error];
if (error) {
    NSLog(@"ERROR!!! --> %@", error.localizedDescription);
} else {
    [cdh.context refreshObject:item.photo mergeChanges:NO];
    [cdh.context refreshObject:item mergeChanges:NO];
}


Summary

This chapter has shown what to look for when using Instruments to measure Core Data performance. The various options of NSFetchRequest have been explained and their merits demonstrated. It has also been shown how model design plays a key role in the performance of an application, particularly when large objects are involved. Remember that it is important to execute your testing on the slowest possible device with the largest data set your customers are likely to use. If an application works well on a slow device with a large data set, it will perform even better on the latest devices.

Exercises

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

1. Comment out the code to set the request batch size in the configureFetch method of PrepareTVC.m. Check the query execution time and investigate the performance using Instruments.

2. Set the request fetch limit and offset in the configureFetch method of PrepareTVC.m using the code from Listing 10.7. Examine the query execution time and investigate the performance using Instruments. Notice the effect this change has had on the items shown in the Prepare table view.

Listing 10.7 PrepareTVC.m: configureFetch


[request setFetchLimit:20];
[request setFetchOffset:50];