iOS Programming: The Big Nerd Ranch Guide (2014)
23. Core Data
When deciding between approaches to saving and loading for iOS applications, the first question is typically “Local or remote?” If you want to save data to a remote server, this is typically done with a web service. Let’s assume that you want to store data locally. The next question is typically“Archiving or Core Data?”
At the moment, Homepwner uses keyed archiving to save item data to the filesystem. The biggest drawback to archiving is its all-or-nothing nature: to access anything in the archive, you must unarchive the entire file; to save any changes, you must rewrite the entire file. Core Data, on the other hand, can fetch a small subset of the stored objects. And if you change any of those objects, you can update just that part of the file. This incremental fetching, updating, deleting, and inserting can radically improve the performance of your application when you have a lot of model objects being shuttled between the filesystem and RAM.
Object-Relational Mapping
Core Data is a framework that provides object-relational mapping. In other words, Core Data can turn Objective-C objects into data that is stored in a SQLite database file and vice-versa. SQLite is a relational database that is stored in a single file. (Technically, SQLite is the library that manages the database file, but we use the word to mean both the file and the library.) It is important to note that SQLite is not a full-fledged relational database server like Oracle, MySQL, or SQLServer, which are their own applications that clients can connect to over a network.
Core Data gives us the ability to fetch and store data in a relational database without having to know SQL. However, you do have to understand a bit about how relational databases work. This chapter will give you that understanding as you replace keyed archiving with Core Data inHomepwner’s BNRItemStore.
Moving Homepwner to Core Data
Your Homepwner application currently uses archiving to save and reload its data. For a moderately sized object model (say, fewer than 1000 objects), this is fine. As your object model gets larger, however, you will want to be able to do incremental fetches and updates, and Core Data can do this.
The model file
In a relational database, we have something called a table. A table represents some type; you can have a table of people, a table of a credit card purchases, or a table of real-estate listings. Each table has a number of columns to hold pieces of information about that thing. A table that represents people might have columns for the person’s last name, date of birth, and height. Every row in the table represents a single person.
Figure 23.1 Role of Core Data
This organization translates well to Objective-C. Every table is like an Objective-C class. Every column is one of the class’s properties. Every row is an instance of that class. Thus, Core Data’s job is to move data to and from these two representations (Figure 23.1).
Core Data uses different terminology to describe these ideas: a table/class is called a entity, and the columns/properties are called attributes. A Core Data model file is the description of every entity along with its attributes in your application. In Homepwner, you are going to describe a BNRItementity in a model file and give it attributes like itemName, serialNumber, and valueInDollars.
Open Homepwner.xcodeproj. From the File menu, create a new file. Select Core Data in the iOS section and create a new Data Model. Name it Homepwner.
Figure 23.2 Create the model file
This will create a Homepwner.xcdatamodeld file and add it to your project. Select this file from the project navigator, and the editor area will reveal the user interface for manipulating a Core Data model file.
Find the Add Entity button at the bottom left of the window and click it. A new Entity will appear in the list of entities in the lefthand table. Double-click this entity and change its name to BNRItem (Figure 23.3).
Figure 23.3 Create the BNRItem entity
Now your BNRItem entity needs attributes. Remember that these will be the properties of the BNRItem class. The necessary attributes are listed below. For each attribute, click the + button in the Attributes section and edit the Attribute and Type values:
· itemName is a String
· serialNumber is a String
· valueInDollars is an Integer 32
· dateCreated is a Date
· itemKey is a String
· thumbnail is a Transformable (It is a UIImage, but that is not one of the possibilities. We will discuss Transformable shortly.)
There is one more attribute to add. In Homepwner, users can order items by changing their positions in the table view. Archiving items in an array naturally respects this order. However, relational tables do not order their rows. Instead, when you fetch a set of rows, you specify their order using one of the attributes (“Fetch me all the BNREmployee objects ordered by lastName.”).
To maintain the order of items, you need to create an attribute to record each item’s position in the table view. Then when you fetch items, you can ask for them to be ordered by this attribute. (You will also need to update that attribute when the items are reordered.) Create this final attribute: name it orderingValue and make it a Double.
Core Data is only able to store certain data types in its store, and UIImage is not one of these types. Instead, you declared the thumbnail as transformable. With a transformable attribute, Core Data will convert the object into NSData when saving, and convert the NSData back into the original object when loading it from the file system. In order for Core Data to do this, you have to supply it with an NSValueTransformer subclass that handles these conversions.
Create a new class named BNRImageTransformer that is a subclass of NSValueTransformer. Open BNRImageTransformer.m and override the methods necessary for transforming the UIImage to and from NSData.
@implementation BNRImageTransformer
+ (Class)transformedValueClass
{
return [NSData class];
}
- (id)transformedValue:(id)value
{
if (!value) {
return nil;
}
if ([value isKindOfClass:[NSData class]]) {
return value;
}
return UIImagePNGRepresentation(value);
}
- (id)reverseTransformedValue:(id)value
{
return [UIImage imageWithData:value];
}
@end
The implementation of BNRImageTransformer is pretty straightforward. The class method transformedValueClass tells the transformer what type of object it will receive from the transformedValue: method. The transformedValue: method will be called when your transformable variable is to be saved to the file system, and it expects an object that can be saved to Core Data. In this example, the argument to the method will be a UIImage and it will return an instance of NSData. Finally, the reverseTransformedValue: method is called when the thumbnail data is loaded from the file system, and your implementation will create the UIImage from the NSData that was stored. With this file created, Core Data must know to use this class when working with the thumbnail.
Open Homepwner.xcdatamodeld and select the BNRItem entity. Select thumbnail from the Attributes list and then click the tab in the inspector selector to show the data model inspector. Replace the Value Transformer Name placeholder text in the second Name field with BNRImageTransformer (Figure 23.4).
Figure 23.4 Update value transformer name for thumbnail attribute
At this point, your model file is sufficient to save and load items. However, one of the benefits to using Core Data is that entities can be related to one another, so you are going to add a new entity called BNRAssetType that describes a category of items. For example, a painting might be of theArt asset type. BNRAssetType will be an entity in the model file, and each row of that table will be mapped to an Objective-C object at runtime.
In Homepwner.xcdatamodeld, add another entity called BNRAssetType. Give it an attribute called label of type String. This will be the name of the category the BNRAssetType represents.
Figure 23.5 Create the BNRAssetType entity
Now you need to establish relationships between BNRAssetType and BNRItem. Relationships between entities are represented by pointers between objects. There are two kinds of relationships: to-one and to-many.
When an entity has a to-one relationship, each instance of that entity will have a pointer to an instance in the entity it has a relationship to. The BNRItem entity will have a to-one relationship to the BNRAssetType entity. Thus, a BNRItem instance will have a pointer to a BNRAssetType instance.
When an entity has a to-many relationship, each instance of that entity has a pointer to an NSSet. This set contains the instances of the entity that it has a relationship with. The BNRAssetType entity will have a to-many relationship to the BNRItem entity because many instances of BNRItem can have the same BNRAssetType. Thus, a BNRAssetType object will have a pointer to a set of all of the BNRItem objects that are its type of asset.
With these relationships set up, you can ask a BNRAssetType object for the set of BNRItem objects that fall into its category, and you can ask a BNRItem which BNRAssetType it falls under. Figure 23.6 diagrams the relationships between BNRAssetType and BNRItem.
Figure 23.6 Entities in Homepwner
Let’s add these relationships to the model file. Select the BNRAssetType entity and then click the + button in the Relationships section. Name this relationship items in the Relationship column. Then, select BNRItem from the Destination column. In the data model inspector, change the Type drop-down from To One to To Many (Figure 23.7).
Figure 23.7 Create the items relationship
Now go back to the BNRItem entity. Add a relationship named assetType and pick BNRAssetType as its destination. In the Inverse column, select items (Figure 23.8).
Figure 23.8 Create the assetType relationship
NSManagedObject and subclasses
When an object is fetched with Core Data, its class, by default, is NSManagedObject. NSManagedObject is a subclass of NSObject that knows how to cooperate with the rest of Core Data. An NSManagedObject works a bit like a dictionary: it holds a key-value pair for every property (attribute or relationship) in the entity.
An NSManagedObject is little more than a data container. If you need your model objects to do something in addition to holding data, you must subclass NSManagedObject. Then, in your model file, you specify that this entity is represented by instances of your subclass, not the standardNSManagedObject.
Select the BNRItem entity. Show the data model inspector and change the Class field to BNRItem, as shown in Figure 23.9. Now, when a BNRItem entity is fetched with Core Data, the type of this object will be BNRItem. (BNRAssetType instances will still be of type NSManagedObject.)
Figure 23.9 Changing the class of an entity
There is one problem: the BNRItem class already exists, and it does not inherit from NSManagedObject. Changing the superclass of the existing BNRItem to NSManagedObject will require considerable modifications. Thus, the easiest solution is to remove your current BNRItem class files, have Core Data generate a new BNRItem class, and then add your behavior methods back to the new class files.
In Finder, drag both BNRItem.h and BNRItem.m to your desktop for safekeeping. Then, in Xcode, delete these two files from the project navigator. (They will appear in red after you have moved the files).
Now, open Homepwner.xcdatamodeld again and select the BNRItem entity. Then, select New File... from the New menu.
From the iOS section, select Core Data, choose the NSManagedObject subclass option, and click Next. The checkbox for the Homepwner data model should already be checked. If it is not checked, go ahead and check the box, and then click Next. On the following screen, make sure that the BNRItem is checked, and then click Next one last time. Finally, click Create to generate the NSManagedObject subclass files.
Xcode will generate new BNRItem.h and BNRItem.m files. Open BNRItem.h and see what Core Data has wrought. Change the type of the thumbnail property to UIImage and add the method declaration from the previous BNRItem. By default, Xcode generates the properties as objects, so your ints are now instances of NSNumber. Change orderingValue to be a double and valueInDollars to be an int.
#import <Foundation/Foundation.h>
@import CoreData;
@interface BNRItem : NSManagedObject
@property (nonatomic, strong) NSDate * dateCreated;
@property (nonatomic, strong) NSString * itemKey;
@property (nonatomic, strong) NSString * itemName;
@property (nonatomic, strong) NSNumber * orderingValue;
@property (nonatomic) double orderingValue;
@property (nonatomic, strong) NSString * serialNumber;
@property (nonatomic, strong) id thumbnail;
@property (nonatomic, strong) UIImage *thumbnail;
@property (nonatomic, strong) NSData * thumbnailData;
@property (nonatomic, strong) NSNumber * valueInDollars;
@property (nonatomic) int valueInDollars;
@property (nonatomic, strong) NSManagedObject *assetType;
- (void)setThumbnailFromImage:(UIImage *)image;
@end
(Xcode might have created the strong properties as retain. These represent the same thing; before ARC, strong properties were called retain, and not all parts of the tools have been updated to the new terminology.)
Copy the setThumbnailFromImage: method from your old BNRItem.m to the new one:
- (void)setThumbnailFromImage:(UIImage *)image
{
CGSize origImageSize = image.size;
CGRect newRect = CGRectMake(0, 0, 40, 40);
float ratio = MAX(newRect.size.width / origImageSize.width,
newRect.size.height / origImageSize.height);
UIGraphicsBeginImageContextWithOptions(newRect.size, NO, 0.0);
UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:newRect
cornerRadius:5.0];
[path addClip];
CGRect projectRect;
projectRect.size.width = ratio * origImageSize.width;
projectRect.size.height = ratio * origImageSize.height;
projectRect.origin.x = (newRect.size.width - projectRect.size.width) / 2.0;
projectRect.origin.y = (newRect.size.height - projectRect.size.height) / 2.0;
[image drawInRect:projectRect];
UIImage *smallImage = UIGraphicsGetImageFromCurrentImageContext();
self.thumbnail = smallImage;
UIGraphicsEndImageContext();
}
Of course, when you first launch an application, there are no saved items or asset types. When the user creates a new BNRItem instance, it will be added to the database. When objects are added to the database, they are sent the message awakeFromInsert. Here is where you will set the dateCreatedand itemKey properties of a BNRItem. Implement awakeFromInsert in BNRItem.m.
- (void)awakeFromInsert
{
[super awakeFromInsert];
self.dateCreated = [NSDate date];
// Create an NSUUID object - and get its string representation
NSUUID *uuid = [[NSUUID alloc] init];
NSString *key = [uuid UUIDString];
self.itemKey = key;
}
This adds the extra behavior of BNRItem’s old designated initializer. Build the application to check for syntax errors, but do not run it.
Updating BNRItemStore
The portal through which you talk to the database is the NSManagedObjectContext. The NSManagedObjectContext uses an NSPersistentStoreCoordinator. You ask the persistent store coordinator to open a SQLite database at a particular filename. The persistent store coordinator uses the model file in the form of an instance of NSManagedObjectModel. In Homepwner, these objects will work with the BNRItemStore. These relationships are shown in Figure 23.10.
Figure 23.10 BNRItemStore and NSManagedObjectContext
In BNRItemStore.m, import Core Data and add three properties to the class extension.
@import CoreData;
@interface BNRItemStore ()
@property (nonatomic) NSMutableArray *privateItems;
@property (nonatomic, strong) NSMutableArray *allAssetTypes;
@property (nonatomic, strong) NSManagedObjectContext *context;
@property (nonatomic, strong) NSManagedObjectModel *model;
Then change the implementation of itemArchivePath to return a different path that Core Data will use to save data.
- (NSString *)itemArchivePath
{
NSArray *documentDirectories =
NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,
NSUserDomainMask,
YES);
// Get one and only document directory from that list
NSString *documentDirectory = [documentDirectories firstObject];
return [documentDirectory stringByAppendingPathComponent:@"items.archive"];
return [documentDirectory stringByAppendingPathComponent:@"store.data"];
}
When the BNRItemStore is initialized, it needs to set up the NSManagedObjectContext and an NSPersistentStoreCoordinator. The persistent store coordinator needs to know two things: “What are all of my entities and their attributes and relationships?” and “Where am I saving and loading data from?” To answer these questions, you need to create an instance of NSManagedObjectModel to hold the entity information of Homepwner.xcdatamodeld and initialize the persistent store coordinator with this object. Then, you will create the instance of NSManagedObjectContext and specify that it use this persistent store coordinator to save and load objects.
In BNRItemStore.m, update initPrivate.
- (instancetype)initPrivate
{
self = [super init];
if (self) {
NSString *path = self.itemArchivePath;
_privateItems = [NSKeyedUnarchiver unarchiveObjectWithFile:path];
if (!_privateItems) {
_privateItems = [[NSMutableArray alloc] init];
}
// Read in Homepwner.xcdatamodeld
_model = [NSManagedObjectModel mergedModelFromBundles:nil];
NSPersistentStoreCoordinator *psc =
[[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:_model];
// Where does the SQLite file go?
NSString *path = self.itemArchivePath;
NSURL *storeURL = [NSURL fileURLWithPath:path];
NSError *error = nil;
if (![psc addPersistentStoreWithType:NSSQLiteStoreType
configuration:nil
URL:storeURL
options:nil
error:&error]) {
@throw [NSException exceptionWithName:@"OpenFailure"
reason:[error localizedDescription]
userInfo:nil];
}
// Create the managed object context
_context = [[NSManagedObjectContext alloc] init];
_context.persistentStoreCoordinator = psc;
}
return self;
}
Before, BNRItemStore would write out the entire NSMutableArray of BNRItem objects when you asked it to save using keyed archiving. Now, you will have it send the message save: to the NSManagedObjectContext. The context will update all of the records in store.data with any changes since the last time it was saved. In BNRItemStore.m, change saveChanges.
- (BOOL)saveChanges
{
NSString *path = [self itemArchivePath];
return [NSKeyedArchiver archiveRootObject:allItems
toFile:[self itemArchivePath]];
NSError *error;
BOOL successful = [self.context save:&error];
if (!successful) {
NSLog(@"Error saving: %@", [error localizedDescription]);
}
return successful;
}
Recall that this method is already called when the application is moved to the background.
NSFetchRequest and NSPredicate
In this application, you will fetch all of the items in store.data the first time you need them. To get objects back from the NSManagedObjectContext, you must prepare and execute an NSFetchRequest. After a fetch request is executed, you will get an array of all the objects that match the parameters of that request.
A fetch request needs an entity description that defines which entity you want to get objects from. To fetch BNRItem instances, you specify the BNRItem entity. You can also set the request’s sort descriptors to specify the order of the objects in the array. A sort descriptor has a key that maps to an attribute of the entity and a BOOL that indicates if the order should be ascending or descending. You want to sort the returned instances of BNRItem by orderingValue in ascending order.
In BNRItemStore.m, define a new method, loadAllItems, to prepare and execute the fetch request and save the results into the allItems array.
- (void)loadAllItems
{
if (!self.privateItems) {
NSFetchRequest *request = [[NSFetchRequest alloc] init];
NSEntityDescription *e = [NSEntityDescription entityForName:@"BNRItem"
inManagedObjectContext:self.context];
request.entity = e;
NSSortDescriptor *sd = [NSSortDescriptor
sortDescriptorWithKey:@"orderingValue"
ascending:YES];
request.sortDescriptors = @[sd];
NSError *error;
NSArray *result = [self.context executeFetchRequest:request error:&error];
if (!result) {
[NSException raise:@"Fetch failed"
format:@"Reason: %@", [error localizedDescription]];
}
self.privateItems = [[NSMutableArray alloc] initWithArray:result];
}
}
Also in BNRItemStore.m, send this message to the BNRItemStore at the end of initPrivate.
_context.persistentStoreCoordinator = psc;
[self loadAllItems];
}
return self;
}
You can build to check for syntax errors.
In this application, you immediately fetched all the instances of the BNRItem entity. This is a simple request. In an application with a much larger data set, you would carefully fetch just the instances you needed. To selectively fetch instances, you add a predicate (an NSPredicate) to your fetch request, and only the objects that satisfy the predicate are returned.
A predicate contains a condition that can be true or false. For example, if you only wanted the items worth more than $50, you would create a predicate and add it to the fetch request like this:
NSPredicate *p = [NSPredicate predicateWithFormat:@"valueInDollars > 50"];
[request setPredicate:p];
The format string for a predicate can be very long and complex. Apple’s Predicate Programming Guide is a complete discussion of what is possible.
Predicates can also be used to filter the contents of an array. So, even if you had already fetched the allItems array, you could still use a predicate:
NSArray *expensiveStuff = [allItems filteredArrayUsingPredicate:p];
Adding and deleting items
Thus far, you have taken care of saving and loading, but what about adding and deleting? When the user wants to create a new BNRItem, you will not allocate and initialize this new BNRItem. Instead, you will ask the NSManagedObjectContext to insert a new object from the BNRItem entity. It will then return an instance of BNRItem.
In BNRItemStore.m, edit the createItem method.
- (BNRItem *)createItem
{
BNRItem *item = [[BNRItem alloc] init];
double order;
if ([self.allItems count] == 0) {
order = 1.0;
} else {
order = [[self.privateItems lastObject] orderingValue] + 1.0;
}
NSLog(@"Adding after %d items, order = %.2f", [self.privateItems count], order);
BNRItem *item = [NSEntityDescription insertNewObjectForEntityForName:@"BNRItem"
inManagedObjectContext:self.context];
item.orderingValue = order;
[self.privateItems addObject:item];
return item;
}
When a user deletes a BNRItem, you must inform the context so that it is removed from the database. In BNRItemStore.m, add the following code to removeItem:.
- (void)removeItem:(BNRItem *)item
{
NSString *key = item.itemKey;
[[BNRImageStore sharedStore] deleteImageForKey:key];
[self.context deleteObject:item];
[self.privateItems removeObjectIdenticalTo:item];
}
Reordering items
The last bit of functionality you need to replace for BNRItem is the ability to re-order items in the BNRItemStore. Because Core Data will not handle ordering automatically, you must update a BNRItem’s orderingValue every time it is moved in the table view.
This would get rather complicated if the orderingValue was an integer: every time a BNRItem was placed in a new index, you would have to change the orderingValue’s of other items. This is why you created orderingValue as a double. You can take the orderingValues of the items that will be before and after the moving item, add them together, and divide by two. The new orderingValue will fall directly in between the values of the items that surround it.
In BNRItemStore.m, modify moveItemAtIndex:toIndex: to handle reordering items.
- (void)moveItemAtIndex:(NSUInteger)fromIndex
toIndex:(NSUInteger)toIndex
{
if (fromIndex == toIndex) {
return;
}
BNRItem *item = self.privateItems[fromIndex];
[self.privateItems removeObjectAtIndex:fromIndex];
[self.privateItems insertObject:item atIndex:toIndex];
// Computing a new orderValue for the object that was moved
double lowerBound = 0.0;
// Is there an object before it in the array?
if (toIndex > 0) {
lowerBound = [self.privateItems[(toIndex - 1)] orderingValue];
} else {
lowerBound = [self.privateItems[1] orderingValue] - 2.0;
}
double upperBound = 0.0;
// Is there an object after it in the array?
if (toIndex < [self.privateItems count] - 1) {
upperBound = [self.privateItems[(toIndex + 1)] orderingValue];
} else {
upperBound = [self.privateItems[(toIndex - 1)] orderingValue] + 2.0;
}
double newOrderValue = (lowerBound + upperBound) / 2.0;
NSLog(@"moving to order %f", newOrderValue);
item.orderingValue = newOrderValue;
}
Finally, you can build and run your application. Of course, the behavior is the same as it always was, but it is now using Core Data.
Adding BNRAssetTypes to Homepwner
In the model file, you described a new entity, BNRAssetType, that every item will have a to-one relationship to. You need a way for the user to set the BNRAssetType of a BNRItem. Also, the BNRItemStore will need a way to fetch the asset types. (Creating new instances of BNRAssetType is left as a challenge at the end of this chapter.)
In BNRItemStore.h, declare a new method.
- (NSArray *)allAssetTypes;
In BNRItemStore.m, define this method. If this is the first time the application is being run – and therefore there are no BNRAssetType objects in the store – create three default types.
- (NSArray *)allAssetTypes
{
if (!_allAssetTypes) {
NSFetchRequest *request = [[NSFetchRequest alloc] init];
NSEntityDescription *e = [NSEntityDescription entityForName:@"BNRAssetType"
inManagedObjectContext:self.context];
request.entity = e;
NSError *error = nil;
NSArray *result = [self.context executeFetchRequest:request error:&error];
if (!result) {
[NSException raise:@"Fetch failed"
format:@"Reason: %@", [error localizedDescription]];
}
_allAssetTypes = [result mutableCopy];
}
// Is this the first time the program is being run?
if ([_allAssetTypes count] == 0) {
NSManagedObject *type;
type = [NSEntityDescription insertNewObjectForEntityForName:@"BNRAssetType"
inManagedObjectContext:self.context];
[type setValue:@"Furniture" forKey:@"label"];
[_allAssetTypes addObject:type];
type = [NSEntityDescription insertNewObjectForEntityForName:@"BNRAssetType"
inManagedObjectContext:self.context];
[type setValue:@"Jewelry" forKey:@"label"];
[_allAssetTypes addObject:type];
type = [NSEntityDescription insertNewObjectForEntityForName:@"BNRAssetType"
inManagedObjectContext:self.context];
[type setValue:@"Electronics" forKey:@"label"];
[_allAssetTypes addObject:type];
}
return _allAssetTypes;
}
Now you need to change the user interface so that the user can see and change the BNRAssetType of the BNRItem in the BNRDetailViewController.
Figure 23.11 Interface for BNRAssetType
Create a new Objective-C class template file and choose NSObject as the superclass. Name this class BNRAssetTypeViewController.
In BNRAssetTypeViewController.h, forward declare BNRItem, change the superclass to UITableViewController, and give it a BNRItem property.
#import <Foundation/Foundation.h>
@class BNRItem;
@interface BNRAssetTypeViewController : NSObject
@interface BNRAssetTypeViewController : UITableViewController
@property (nonatomic, strong) BNRItem *item;
@end
This table view controller will show a list of the available asset types. Tapping a button on the BNRDetailViewController’s view will display it. Implement the data source methods and import the appropriate header files in BNRAssetTypeViewController.m. (You have seen all this before.)
#import "BNRAssetTypeViewController.h"
#import "BNRItemStore.h"
#import "BNRItem.h"
@implementation BNRAssetTypeViewController
- (instancetype)init
{
return [super initWithStyle:UITableViewStylePlain];
}
- (instancetype)initWithStyle:(UITableViewStyle)style
{
return [self init];
}
- (void)viewDidLoad
{
[super viewDidLoad];
[self.tableView registerClass:[UITableViewCell class]
forCellReuseIdentifier:@"UITableViewCell"];
}
- (NSInteger)tableView:(UITableView *)tableView
numberOfRowsInSection:(NSInteger)section
{
return [[[BNRItemStore sharedStore] allAssetTypes] count];
}
- (UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCell *cell =
[tableView dequeueReusableCellWithIdentifier:@"UITableViewCell"
forIndexPath:indexPath];
NSArray *allAssets = [[BNRItemStore sharedStore] allAssetTypes];
NSManagedObject *assetType = allAssets[indexPath.row];
// Use key-value coding to get the asset type's label
NSString *assetLabel = [assetType valueForKey:@"label"];
cell.textLabel.text = assetLabel;
// Checkmark the one that is currently selected
if (assetType == self.item.assetType) {
cell.accessoryType = UITableViewCellAccessoryCheckmark;
} else {
cell.accessoryType = UITableViewCellAccessoryNone;
}
return cell;
}
- (void)tableView:(UITableView *)tableView
didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath];
cell.accessoryType = UITableViewCellAccessoryCheckmark;
NSArray *allAssets = [[BNRItemStore sharedStore] allAssetTypes];
NSManagedObject *assetType = allAssets[indexPath.row];
self.item.assetType = assetType;
[self.navigationController popViewControllerAnimated:YES];
}
@end
In BNRDetailViewController.xib, drag a UIBarButtonItem onto the toolbar. Create an outlet to this button by selecting the toolbar then the new bar button and Control-dragging to the class extension of BNRDetailViewController.m. Name this outlet assetTypeButton. Then, create an action from this button in the same way by dragging into the @implementation section instead of the class extension and name it showAssetTypePicker.
The following method and instance variable should now be declared in BNRDetailViewController.m:
@property (weak, nonatomic) IBOutlet UIBarButtonItem *assetTypeButton;
@end
@implementation BNRDetailViewController
// Other methods here
- (IBAction)showAssetTypePicker:(id)sender
{
}
@end
At the top of BNRDetailViewController.m, import the header for this new table view controller.
#import "BNRDetailViewController.h"
#import "BNRAssetTypeViewController.h"
Finish implementing showAssetTypePicker: in BNRDetailViewController.m.
- (IBAction)showAssetTypePicker:(id)sender
{
[self.view endEditing:YES];
BNRAssetTypeViewController *avc = [[BNRAssetTypeViewController alloc] init];
avc.item = self.item;
[self.navigationController pushViewController:avc
animated:YES];
}
And finally, update the title of the button to show the asset type of a BNRItem. In BNRDetailViewController.m, add the following code to viewWillAppear:.
if (self.itemKey) {
// Get image for image key from image cache
UIImage *imageToDisplay = [[BNRImageStore sharedStore]
imageForKey:self.itemKey];
// Use that imge to put on the screen in imageView
self.imageView.image = imageToDisplay;
} else {
// clear the imageView
imageView.image = nil;
}
NSString *typeLabel = [self.item.assetType valueForKey:@"label"];
if (!typeLabel) {
typeLabel = @"None";
}
self.assetTypeButton.title = [NSString stringWithFormat:@"Type: %@", typeLabel];
[self updateFonts];
}
Build and run the application. Select a BNRItem and set its asset type.
More About SQL
In this chapter, you used SQLite via Core Data. If you are curious about what SQL commands Core Data is executing, you can use a command-line argument to log all communications with the SQLite database to the console. From the Product menu, choose Edit Scheme.... Select the Run Homepwner.app item and the Arguments tab. Add two arguments: -com.apple.CoreData.SQLDebug and 1, as shown.
Figure 23.12 Turning on Core Data logging
Build and run the application again. Make sure the debug area and console are visible so you can see the SQL logging. Add a few locations and inventory items. Then navigate around the application looking at various items.
Faults
Relationships are fetched in a lazy manner. When you fetch a managed object with relationships, the objects at the other end of those relationships are not fetched. Instead, Core Data uses faults. Faults are lightweight placeholder objects that provide an endpoint for a relationship until the potentially larger objects are actually needed. This provides both performance and memory usage boons to object management.
There are to-many faults (which stand in for sets) and to-one faults (which stand in for managed objects). So, for example, when the instances of BNRItem are fetched into your application, the instances of BNRAssetType are not. Instead, fault objects are created that stand in for the BNRAssetTypeobjects until they are really needed.
Figure 23.13 Object faults
An object fault knows what entity it is from and what its primary key is. So, for example, when you ask a fault that represents an asset type what its label is, you will see SQL executed that looks something like this:
SELECT t0.Z_PK, t0.Z_OPT, t0.ZLABEL FROM ZBNRASSETTYPE t0 WHERE t0.Z_PK = 2
(Why is everything prefixed with Z_? We do not know. What is OPT? We guess it is short for “optimistic locking.” These details are not important.) The fault is replaced, in the exact same location in memory, with a managed object containing the real data.
Figure 23.14 After one fault is replaced
This lazy fetching makes Core Data not only easy to use, but also quite efficient.
What about to-many faults? Imagine that your application worked the other way: the user is presented with a list of asset types to select from. Then, the items for that asset type are fetched and displayed. How would this work? When the assets are first fetched, each one has a set fault that is standing in for the NSSet of item objects:
Figure 23.15 Set faults
When the set fault is sent a message that requires the BNRItem objects, it fetches them and replaces itself with an NSSet:
Figure 23.16 Set fault replaced
Core Data is a very powerful and flexible persistence framework, and this chapter has been just a quick introduction to its capabilities. For more details, we strongly suggest that you read Apple’s Core Data Programming Guide. Here are some of the things we have not delved into:
· NSFetchRequest is a powerful mechanism for specifying data you want from the persistent store. We used it a little, but you will want to go deeper. You should also explore the following related classes: NSPredicate, NSSortOrdering, NSExpressionDescription, and NSExpression. Also, fetch request templates can be created as part of the model file.
· A fetched property is a little like a to-many relationship and a little like an NSFetchRequest. You typically specify them in the model file.
· As your app evolves from version to version, you will need to change the data model over time. This can be tricky – in fact, Apple has an entire guide about it: Data Model Versioning and Data Migration Programming Guide.
· There is good support for validating data as it goes into your instances of NSManagedObject and again as it moves from your managed object into the persistent store.
· You can have a single NSManagedObjectContext working with more than one persistent store. You partition your model into configurations and then assign each configuration to a particular persistent store. You are not allowed to have relationships between entities in different stores, but you can use fetched properties to achieve a similar result.
Trade-offs of Persistence Mechanisms
At this point, you can start thinking about the trade-offs between the common ways that iOS applications can store their data. Which is best for your application? Use Table 23.1 to help you decide.
Table 23.1 Data storage pros and cons
Technique |
Pros |
Cons |
Archiving |
Allows ordered relationships (arrays, not sets). Easy to deal with versioning. |
Reads all the objects in (no faulting). No incremental updates. |
Web Service |
Makes it easy to share data with other devices and applications. |
Requires a server and a connection to the Internet. |
Core Data |
Lazy fetches by default. Incremental updates. |
Versioning is awkward (but can certainly be done using an NSModelMapping). No real ordering within an entity, although to-many relationships can be ordered. |
Bronze Challenge: Assets on the iPad
On the iPad, present the BNRAssetTypeViewController in a UIPopoverController.
Silver Challenge: New Asset Types
Make it possible for the user to add new asset types by adding a button to the BNRAssetTypeViewController’s navigationItem.
Gold Challenge: Showing Assets of a Type
In the BNRAssetTypeViewController view controller, create a second section in the table view. This section should show all of the assets that belong to the selected asset type.