iOS Programming: The Big Nerd Ranch Guide, 3/e (Big Nerd Ranch Guides) (61 page)

Read iOS Programming: The Big Nerd Ranch Guide, 3/e (Big Nerd Ranch Guides) Online

Authors: Aaron Hillegass,Joe Conway

Tags: #COM051370, #Big Nerd Ranch Guides, #iPhone / iPad Programming

BOOK: iOS Programming: The Big Nerd Ranch Guide, 3/e (Big Nerd Ranch Guides)
9.06Mb size Format: txt, pdf, ePub
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 16.11
.

 

Figure 16.11  BNRItemStore and NSManagedObjectContext

 
 

In
BNRItemStore.h
, import Core Data and add three instance variables.

 
#import
@class BNRItem;
@interface BNRItemStore : NSObject
{
    NSMutableArray *allItems;
    
NSMutableArray *allAssetTypes;
    NSManagedObjectContext *context;
    NSManagedObjectModel *model;
}
 

In
BNRItemStore.m
, 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 objectAtIndex:0];
    
return [documentDirectory stringByAppendingPathComponent:@"items.archive"];
    
return [documentDirectory stringByAppendingPathComponent:@"store.data"];
}
 

When the
BNRItemStore
is initialized, it needs to set up the
NSManagedObjectContext
and a
NSPersistentStoreCoordinator
. The persistent store coordinator needs to know two things:

What are all of my entities and their attribute and relationships?

and

Where am I saving and loading data from?

To answer these questions, we 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, we will create the instance of
NSManagedObjectContext
and specify that it use this persistent store coordinator to save and load objects.

 

In
BNRItemStore.m
, update
init
.

 
- (id)init
{
    self = [super init];
    if (self) {
        
NSString *path = [self itemArchivePath];
        
allItems = [NSKeyedUnarchiver unarchiveObjectWithFile:path];
        
if (!allItems)
            
allItems = [[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]) {
            [NSException raise:@"Open failed"
                        format:@"Reason: %@", [error localizedDescription]];
        }
        // Create the managed object context
        context = [[NSManagedObjectContext alloc] init];
        [context setPersistentStoreCoordinator:psc];
        // The managed object context can manage undo, but we don't need it
        [context setUndoManager:nil];
    }
    return self;
}
 

Before,
BNRItemStore
would write out the entire
NSMutableArray
of
BNRItem
s 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 *err = nil;
    BOOL successful = [context save:&err];
    if (!successful) {
        NSLog(@"Error saving: %@", [err localizedDescription]);
    }
    return successful;
}

Note that this method is already called when the application is moved to the background.

 
NSFetchRequest and NSPredicate

In this application, we will fetch all of the
BNRItem
s in
store.data
the first time we 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. We want to sort the returned
BNRItem
s by
orderingValue
in ascending order. In
BNRItemStore.h
, declare a new method.

 
- (void)loadAllItems;
 

In
BNRItemStore.m
, define
loadAllItems
to prepare and execute the fetch request and save the results into the
allItems
array.

 
- (void)loadAllItems
{
    if (!allItems) {
        NSFetchRequest *request = [[NSFetchRequest alloc] init];
        NSEntityDescription *e = [[model entitiesByName] objectForKey:@"BNRItem"];
        [request setEntity:e];
        NSSortDescriptor *sd = [NSSortDescriptor
                                    sortDescriptorWithKey:@"orderingValue"
                                                ascending:YES];
        [request setSortDescriptors:[NSArray arrayWithObject:sd]];
        NSError *error;
        NSArray *result = [context executeFetchRequest:request error:&error];
        if (!result) {
                    [NSException raise:@"Fetch failed"
                                format:@"Reason: %@", [error localizedDescription]];
        }
        allItems = [[NSMutableArray alloc] initWithArray:result];
    }
}
 

In
BNRItemStore.m
, send this message to the
BNRItemStore
at the end of
init
.

 
        [context setUndoManager:nil];
        [self loadAllItems];
    }
    return self;
}

You can build to check for syntax errors. You will see a warning that
allAssetTypes
hasn’t been implemented yet – that’s okay for now.

 

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

This handles 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 *p = [[BNRItem alloc] init];
    
double order;
    if ([allItems count] == 0) {
        order = 1.0;
    } else {
        order = [[allItems lastObject] orderingValue] + 1.0;
    }
    NSLog(@"Adding after %d items, order = %.2f", [allItems count], order);
    BNRItem *p = [NSEntityDescription insertNewObjectForEntityForName:@"BNRItem"
                                                inManagedObjectContext:context];
    [p setOrderingValue:order];
    [allItems addObject:p];
    return p;
}
 

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 *)p
{
    NSString *key = [p imageKey];
    [[BNRImageStore sharedStore] deleteImageForKey:key];
    
[context deleteObject:p];
    [allItems removeObjectIdenticalTo:p];
}
 
Reordering items

The last bit of functionality you need to replace for
BNRItem
is the ability to re-order
BNRItem
s in the
BNRItemStore
. Because Core Data will not handle ordering automatically, we 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, we would have to change the
orderingValue
’s of other
BNRItem
s. This is why we created
orderingValue
as a
double
. We can take the
orderingValue
s of the
BNRItem
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
BNRItem
s that surround it. In
BNRItemStore.m
, modify
moveItemAtIndex:toIndex:
to handle reordering items.

 
- (void)moveItemAtIndex:(int)from
                toIndex:(int)to
{
    if (from == to) {
        return;
    }
    BNRItem *p = [allItems objectAtIndex:from];
    [allItems removeObjectAtIndex:from];
    [allItems insertObject:p atIndex:to];
    
// Computing a new orderValue for the object that was moved
    double lowerBound = 0.0;
    // Is there an object before it in the array?
    if (to > 0) {
        lowerBound = [[allItems objectAtIndex:to - 1] orderingValue];
    } else {
        lowerBound = [[allItems objectAtIndex:1] orderingValue] - 2.0;
    }
    double upperBound = 0.0;
    // Is there an object after it in the array?
    if (to < [allItems count] - 1) {
        upperBound = [[allItems objectAtIndex:to + 1] orderingValue];
    } else {
        upperBound = [[allItems objectAtIndex:to - 1] orderingValue] + 2.0;
    }
    double newOrderValue = (lowerBound + upperBound) / 2.0;
    NSLog(@"moving to order %f", newOrderValue);
    [p setOrderingValue: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.

 

Other books

Remy by Katy Evans
Storm Tide by Elisabeth Ogilvie
Bride of Fire by Teglia, Charlene
Dead, but Not for Long by Kinney, Matthew, Anders, Lesa
Istanbul Passage by Joseph Kanon
Kindred by Nicola Claire
Doc Featherstone's Return by Stephani Hecht