Episode #109

MDMCoreData

21 minutes
Published on February 27, 2014

This video is only available to subscribers. Get access to this video and 572 others.

In this episode we add Core Data to our Weight Tracker application, but we lean on a new library called MDMCoreData to set up our Core Data stack for us. Using MDMCoreData we can get a sensible parent/child context set up where the main context is a child of the parent writer context. We also leverage a handy class that dovetails with NSFetchedResultsController to display records in a UITableView.

Episode Links

Getting MDMCoreData

You can easily do this by adding it to your Podfile:

pod 'MDMCoreData', '~> 1.0'

Then run pod install.

Setting up the Core Data Stack

We just need to hand over our Model URL and the path to where we want our sqlite store to be:

- (void)setupCoreData {
    NSURL *modelUrl = [[NSBundle mainBundle] URLForResource:@"WeightTracker" withExtension:@"momd"];
    NSURL *storeUrl = [NSURL fileURLWithPath:[[self documentsDirectory] stringByAppendingPathComponent:@"weight.sqlite"]];

    self.persistenceController = [[MDMPersistenceController alloc] initWithStoreURL:storeUrl modelURL:modelUrl];
}

We'll then pass our persistenceController over to any view controller that needs it.

Saving the Weight Log

Here we're saving the value that the user entered in:

- (void)saveWeight:(CGFloat)weight {
    NSEntityDescription *entityDesc = [NSEntityDescription entityForName:@"WeightLog"
                                                  inManagedObjectContext:self.persistenceController.managedObjectContext];
    WTWeightLog *log = [[WTWeightLog alloc] initWithEntity:entityDesc
                            insertIntoManagedObjectContext:self.persistenceController.managedObjectContext];
    log.dateTaken = [NSDate date];
    log.units = @"lbs";
    log.weight = @(weight);

    [self updateForWeight:log];

    NSError *error;
    if([self.persistenceController.managedObjectContext save:&error]) {
        NSLog(@"Saved");

        [self.persistenceController saveContextAndWait:YES completion:^(NSError *error) {
            if (error) {
                NSLog(@"ERROR:%@", [error localizedDescription]);
            }
        }];
    } else {
        NSLog(@"Couldn't save: %@", [error localizedDescription]);
    }
}

Note that we save the main context first, which will only bubble the changes up to the parent writer context. We then tell the persistenceController to save the writer context and let us know if we encountered an error.

We're also updating the UI for this weight as well.

Fetching the Recent Log Entries

When we tap on the info button, we should see a list of all of our log entries in date descending order. We can do this with NSFetchedResultsController.

MDMCoreData has a handy data source class we can use to make this a bit easier. We first declare some properties (and a protocol to conform to):
```objc
@interface WeightEntriesViewController ()

@property (nonatomic, strong) IBOutlet UITableView *tableView;
@property (nonatomic, strong) MDMFetchedResultsTableDataSource *dataSource;
@property (nonatomic, strong) NSFetchedResultsController *fetchedResultsController;

@end
```

Then we set it up in viewDidLoad

    self.fetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:[self recentEntriesFetchRequest]
                                                                        managedObjectContext:self.persistenceController.managedObjectContext
                                                                          sectionNameKeyPath:nil
                                                                                   cacheName:nil];
    NSError *error;
    if (![self.fetchedResultsController performFetch:&error]) {
        NSLog(@"ERROR FETCHING: %@", [error localizedDescription]);
    }

    self.dataSource = [[MDMFetchedResultsTableDataSource alloc] initWithTableView:self.tableView
                                                         fetchedResultsController:self.fetchedResultsController];
    self.dataSource.reuseIdentifier = @"entryCell";
    self.dataSource.delegate = self;

    self.tableView.dataSource = self.dataSource;

The fetch request looks like this:

- (NSFetchRequest *)recentEntriesFetchRequest {
    NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:@"WeightLog"];
    fetchRequest.sortDescriptors = @[ [NSSortDescriptor sortDescriptorWithKey:@"dateTaken" ascending:NO] ];
    return fetchRequest;
}

Then we just implement the delegate methods for MDMFetchedResultsTableDataSourceDelegate.

#pragma mark - MDMFetchedResultsTableDataSourceDelegate

- (void)dataSource:(MDMFetchedResultsTableDataSource *)dataSource configureCell:(id)cell_ withObject:(id)object {
    UITableViewCell *cell = cell_;
    WTWeightLog *log = object;
    cell.textLabel.text = [NSString stringWithFormat:@"%@ %@", log.weight, log.units];
    cell.detailTextLabel.text = [[self dateFormatter] stringFromDate:log.dateTaken];
}

-(void)dataSource:(MDMFetchedResultsTableDataSource *)dataSource deleteObject:(id)object atIndexPath:(NSIndexPath *)indexPath {
    // not supported yet
}

Fetching the Latest Log Entry

To finish up, we need to load the last entry onto the main screen on first launch.

- (void)loadLastWeight {
    NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:@"WeightLog"];
    fetchRequest.sortDescriptors = @[ [NSSortDescriptor sortDescriptorWithKey:@"dateTaken" ascending:NO] ];
    fetchRequest.fetchLimit = 1;

    NSError *error;
    NSArray *results = [self.persistenceController.managedObjectContext executeFetchRequest:fetchRequest error:&error];
    if (!results) {
        NSLog(@"Couldn't fetch last weight: %@", [error localizedDescription]);
    } else {
        WTWeightLog *lastWeight = [results firstObject];
        if (lastWeight) {
            [self updateForWeight:lastWeight];
        }
    }
}

And that's it! MDMCoreData is a new library, but the code is easy to read and offers up some quick wins with dealing with common Core Data cases.