Objective-C/Earthquakes/AAPLQuakesViewController.m

/*
 Copyright (C) 2014 Apple Inc. All Rights Reserved.
 See LICENSE.txt for this sample’s licensing information
 
 Abstract:
 
  View controller to manage a a table view that displays a collection of quakes.
 
  When requested (by clicking the Fetch Quakes button), the controller creates an asynchronous NSURLSession task to retrieve JSON data about earthquakes. Earthquake data are compared with any existing managed objects to determine whether there are new quakes. New managed objects are created to represent new data, and saved to the persistent store on a private queue.
  
 */
 
#import "AAPLQuakesViewController.h"
#import "AAPLQuake.h"
#import "AAPLCoreDataStackManager.h"
 
@interface AAPLQuakesViewController ()
 
@property (weak) IBOutlet NSTableView *tableView;
@property (weak) IBOutlet NSButton *fetchQuakesButton;
 
@property (nonatomic) NSArray *quakes;
@property (nonatomic, readonly) NSManagedObjectContext *managedObjectContext;
 
@end
 
const NSUInteger BatchSize = 128;
 
NSString *const ColumnIdentifierPlace = @"placeName";
NSString *const ColumnIdentifierTime = @"time";
NSString *const ColumnIdentifierMagnitude = @"magnitude";
 
NSManagedObjectContext *privateQueueContext(NSError * __autoreleasing *error);
 
 
@implementation AAPLQuakesViewController
@synthesize managedObjectContext = _context;
 
#pragma mark - View Life Cycle
 
- (void)viewDidLoad {
    [super viewDidLoad];
 
    [self reloadTableView:self];
}
 
#pragma mark - Core Data Batching
 
- (IBAction)fetchQuakes:(id)sender {
    // Ensure the button can't be pressed again until the fetch is complete.
    self.fetchQuakesButton.enabled = NO;
 
    // Create an NSURLSession and then session task to contact the earthquake server and retrieve JSON data.
    NSURL *jsonURL = [NSURL URLWithString:@"http://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/all_month.geojson"];
 
    NSURLSession *session = [NSURLSession sessionWithConfiguration: [NSURLSessionConfiguration ephemeralSessionConfiguration]];
 
    NSURLSessionDataTask *task = [session dataTaskWithURL:jsonURL completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
 
        if (!data) {
            NSLog(@"Error connecting: %@", [error localizedDescription]);
 
            return;
        }
 
        NSError *anyError;
 
        // Create a context on a private queue to fetch existing quakes to compare with incoming data and create new quakes as required.
        NSManagedObjectContext *taskContext = privateQueueContext(&anyError);
        if (!taskContext) {
            NSLog(@"Error creating background fetch context: %@", [anyError localizedDescription]);
 
            return;
        }
 
        NSDictionary *jsonDictionary = [NSJSONSerialization JSONObjectWithData:data options:0 error:&anyError];
 
        if (!jsonDictionary) {
            NSLog(@"Error creating JSON dictionary: %@", [anyError localizedDescription]);
 
            return;
        }
 
        // Sort the dictionaries by code; this way they can be compared in parallel with existing quakes.
        NSArray *featuresArray = jsonDictionary[@"features"];
        NSArray *sortDescriptors = @[[NSSortDescriptor sortDescriptorWithKey:@"properties.code" ascending:YES]];
        featuresArray = [featuresArray sortedArrayUsingDescriptors:sortDescriptors];
 
        NSUInteger totalFeatureCount = featuresArray.count;
 
        NSUInteger numBatches = totalFeatureCount / BatchSize;
        numBatches += totalFeatureCount % BatchSize > 0 ? 1 : 0;
 
        NSUInteger batchNumber;
 
        for (batchNumber = 0; batchNumber < numBatches; batchNumber++) {
            NSInteger rangeStart = batchNumber * BatchSize;
            NSInteger rangeLength = MIN(BatchSize, totalFeatureCount - batchNumber * BatchSize);
 
            NSRange range = NSMakeRange(rangeStart, rangeLength);
            NSArray *featuresBatchArray = [featuresArray subarrayWithRange:range];
 
            // Create a request to fetch existing quakes with the same codes as those in the JSON data.
            // Existing quakes will be updated with new data; if there isn't a match, then create a new quake to represent the event.
            NSFetchRequest *matchingQuakeRequest = [NSFetchRequest fetchRequestWithEntityName:@"Quake"];
 
            // Get the codes for each of the features and store them in an array.
            NSArray *codes = [featuresBatchArray valueForKeyPath:@"properties.code"];
 
            matchingQuakeRequest.predicate = [NSPredicate predicateWithFormat:@"code in %@" argumentArray:@[codes]];
 
            NSArray *matchingQuakes = [taskContext executeFetchRequest:matchingQuakeRequest error:&anyError];
            if (!matchingQuakes) {
                NSLog(@"Error fetching: %@", [anyError localizedDescription]);
 
                return;
            }
 
            // Create a dictionary to map from a code to the corresponding matched quake.
            NSMutableDictionary *codeToQuakeMap = [[NSMutableDictionary alloc] initWithCapacity:[matchingQuakes count]];
            for (AAPLQuake *quake in matchingQuakes) {
                codeToQuakeMap[quake.code] = quake;
            }
 
            for (NSDictionary *result in featuresBatchArray) {
                // For each feature in turn, retrieve the properties for the quake and create a new quake or update an existing one accordingly.
                NSDictionary * quakeDictionary = result[@"properties"];
                NSString *code = quakeDictionary[@"code"];
 
                // Get the code from the dictionary and use it to look for an existing quake that matched the codes for this batch.
                AAPLQuake *quake = codeToQuakeMap[code];
 
                if (!quake) {
                    quake = (AAPLQuake *)[NSEntityDescription insertNewObjectForEntityForName:@"Quake" inManagedObjectContext:taskContext];
                }
 
                [quake updateFromDictionary:quakeDictionary];
            }
 
            if (![taskContext save:&anyError]) {
                NSLog(@"Error saving batch: %@", [anyError localizedDescription]);
                return;
            }
 
            [taskContext reset];
        }
 
        // Bounce back to the main queue to reload the table view and reenable the fetch button.
        [[NSOperationQueue mainQueue] addOperationWithBlock: ^{
            [self reloadTableView:nil];
            self.fetchQuakesButton.enabled = YES;
        }];
    }];
 
    [task resume];
}
 
#pragma mark - Convenience
 
/// Fetch quakes ordered in time and reload the table view.
- (void)reloadTableView:(id)sender {
    NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"Quake"];
    request.sortDescriptors = @[[NSSortDescriptor sortDescriptorWithKey:@"time" ascending:NO]];
 
    NSError *anyError;
 
    NSArray *fetchedQuakes = [self.managedObjectContext executeFetchRequest:request error:&anyError];
 
    if (!fetchedQuakes) {
        NSLog(@"Error fetching: %@", [anyError localizedDescription]);
 
        return;
    }
 
    self.quakes = fetchedQuakes;
 
    [self.tableView reloadData];
}
 
#pragma mark - Property Overrides
 
/// The managed object context for the view controller (which is bound to the persistent store coordinator for the application).
- (NSManagedObjectContext *)managedObjectContext {
    if (_context) {
        return _context;
    }
    
    _context = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
    _context.persistentStoreCoordinator = [[AAPLCoreDataStackManager sharedManager] persistentStoreCoordinator];
 
    return _context;
}
 
#pragma mark - NSTableViewDataSource
 
-(NSInteger)numberOfRowsInTableView:(NSTableView *)tableView {
    return self.quakes.count;
}
 
#pragma mark - NSTableViewDelegate
 
-(NSView *)tableView:(NSTableView *)tableView viewForTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row {
    NSString *identifier = [tableColumn identifier];
    
    NSTableCellView *cellView = [tableView makeViewWithIdentifier:identifier owner:self];
 
    AAPLQuake *quake = self.quakes[row];
 
    if ([identifier isEqualToString:ColumnIdentifierPlace]) {
        cellView.textField.stringValue = quake.placeName;
    }
    else if ([identifier isEqualToString:ColumnIdentifierTime]) {
        cellView.textField.objectValue = quake.time;
    }
    else if ([identifier isEqualToString:ColumnIdentifierMagnitude]) {
        cellView.textField.objectValue = @(quake.magnitude);
    }
 
    return cellView;
}
 
@end
 
// Creates a new Core Data stack and returns a managed object context associated with a private queue.
NSManagedObjectContext *privateQueueContext(NSError *__autoreleasing *error) {
    // It uses the same store and model, but a new persistent store coordinator and context.
    NSPersistentStoreCoordinator *localCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[AAPLCoreDataStackManager sharedManager].managedObjectModel];
 
    if (![localCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:[AAPLCoreDataStackManager sharedManager].storeURL options:nil error:error]) {
        return nil;
    }
 
    NSManagedObjectContext *context = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
    [context setPersistentStoreCoordinator:localCoordinator];
    context.undoManager = nil;
 
    return context;
}