/* |
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 Cocoa |
|
|
class QuakesViewController: NSViewController, NSTableViewDataSource, NSTableViewDelegate { |
// MARK: Types |
|
private struct Constants { |
static let batchSize = 128 |
} |
|
/// An enumeration to specify the names of earthquake properties that should be displayed in the table view. |
private enum QuakeDisplayProperty: String { |
case Place = "placeName" |
case Time = "time" |
case Magnitude = "magnitude" |
} |
|
// MARK: Properties |
|
@IBOutlet weak var tableView: NSTableView! |
|
@IBOutlet weak var fetchQuakesButton: NSButton! |
|
private var quakes = [Quake]() |
|
/// Managed object context for the view controller (which is bound to the persistent store coordinator for the application). |
private lazy var managedObjectContext: NSManagedObjectContext = { |
let moc = NSManagedObjectContext(concurrencyType: .MainQueueConcurrencyType) |
|
moc.persistentStoreCoordinator = CoreDataStackManager.sharedManager.persistentStoreCoordinator |
|
return moc |
}() |
|
|
// MARK: View Life Cycle |
|
override func viewDidLoad() { |
super.viewDidLoad() |
|
reloadTableView(self) |
} |
|
// MARK: Core Data Batching |
|
@IBAction func fetchQuakes(sender: AnyObject) { |
// Ensure the button can't be pressed again until the fetch is complete. |
fetchQuakesButton.enabled = false |
|
// Create an NSURLSession and then session task to contact the earthquake server and retrieve JSON data. |
let jsonURL = NSURL(string: "http://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/all_month.geojson")! |
|
let sessionConfiguration = NSURLSessionConfiguration.ephemeralSessionConfiguration() |
let session = NSURLSession(configuration: sessionConfiguration) |
|
let task = session.dataTaskWithURL(jsonURL) { data, response, error in |
if data == nil { |
println("Error connecting: \(error)") |
fatalError("Couldn't create connection to server.") |
return |
} |
|
var anyError: NSError? |
|
// Create a context on a private queue to fetch existing quakes to compare with incoming data and create new quakes as required. |
let taskContext = privateQueueContext(&anyError) |
if taskContext == nil { |
println("Error creating fetching context: \(anyError)") |
fatalError("Couldn't create fetching context.") |
return |
} |
|
let jsonDictionary = NSJSONSerialization.JSONObjectWithData(data, options: nil, error: &anyError) as? [NSObject: AnyObject] |
|
if jsonDictionary == nil { |
println("Error creating JSON dictionary: \(anyError)") |
fatalError("Couldn't create JSON dictionary.") |
|
return |
} |
|
var features = jsonDictionary!["features"]! as [[String: AnyObject]] |
let totalFeatureCount = features.count |
|
var numBatches = totalFeatureCount / Constants.batchSize |
numBatches += totalFeatureCount % Constants.batchSize > 0 ? 1 : 0 |
|
for batchNumber in 0..<numBatches { |
let rangeStart = batchNumber * Constants.batchSize |
let rangeEnd = min(rangeStart + Constants.batchSize, totalFeatureCount) |
|
let featuresBatch = Array(features[rangeStart..<rangeEnd]) |
|
// 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. |
let matchingQuakeRequest = NSFetchRequest(entityName: "Quake") |
|
// Get the codes for each of the features and store them in an array. |
let codes = featuresBatch.map { $0["properties"]!["code"] as String } |
matchingQuakeRequest.predicate = NSPredicate(format: "code in %@", argumentArray: [codes]) |
|
let matchingQuakes = taskContext.executeFetchRequest(matchingQuakeRequest, error: &anyError) as? [Quake] |
if matchingQuakes == nil { |
println("Error fetching: \(anyError)") |
fatalError("Fetch failed.") |
return |
} |
|
// Create a dictionary to map from a code to the corresponding matched quake. |
var codeToQuakeMap = [String: Quake](minimumCapacity: matchingQuakes!.count) |
for quake in matchingQuakes! { |
codeToQuakeMap[quake.code] = quake |
} |
|
for result in featuresBatch { |
// For each feature in turn, retrieve the properties for the quake and create a new quake or update an existing one accordingly. |
let quakeDictionary = result["properties"]! as [String: AnyObject] |
var quake: Quake |
|
// Get the code from the dictionary and use it to look for an existing quake that matched the codes for this batch. |
let code = quakeDictionary["code"]! as String |
|
if let existingQuake = codeToQuakeMap[code] { |
quake = existingQuake |
} |
else { |
quake = NSEntityDescription.insertNewObjectForEntityForName("Quake", inManagedObjectContext: taskContext) as Quake |
} |
|
quake.updateFromDictionary(quakeDictionary) |
} |
|
if !taskContext.save(&anyError) { |
println("Error saving batch: \(anyError)") |
fatalError("Saving batch failed.") |
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 = true |
} |
} |
|
task.resume() |
} |
|
// MARK: Convenience |
|
/// Fetch quakes ordered in time and reload the table view. |
private func reloadTableView(sender: AnyObject?) { |
let request = NSFetchRequest(entityName: "Quake") |
request.sortDescriptors = [NSSortDescriptor(key: "time", ascending: false)] |
|
var anyError: NSError? |
|
let fetchedQuakes = managedObjectContext.executeFetchRequest(request, error:&anyError) |
|
if fetchedQuakes == nil { |
println("Error fetching: \(anyError)") |
fatalError("Fetch failed.") |
return |
} |
|
quakes = fetchedQuakes as [Quake] |
|
tableView.reloadData() |
} |
|
// MARK: NSTableViewDataSource |
|
func numberOfRowsInTableView(tableView: NSTableView) -> Int { |
return quakes.count |
} |
|
// MARK: NSTableViewDelegate |
|
func tableView(tableView: NSTableView, viewForTableColumn tableColumn: NSTableColumn, row: Int) -> NSView? { |
let identifier = tableColumn.identifier |
|
if let propertyEnum = QuakeDisplayProperty(rawValue: identifier) { |
let cellView = tableView.makeViewWithIdentifier(identifier, owner: self) as NSTableCellView |
|
let quake = quakes[row] |
|
switch propertyEnum { |
case .Place: |
cellView.textField!.stringValue = quake.placeName |
case .Time: |
cellView.textField!.objectValue = quake.time |
case .Magnitude: |
cellView.textField!.floatValue = quake.magnitude |
} |
|
return cellView |
} |
|
fatalError("Unexpected table column identifier.") |
} |
} |
|
|
// Creates a new Core Data stack and returns a managed object context associated with a private queue. |
private func privateQueueContext(outError: NSErrorPointer) -> NSManagedObjectContext! { |
// It uses the same store and model, but a new persistent store coordinator and context. |
let localCoordinator = NSPersistentStoreCoordinator(managedObjectModel: CoreDataStackManager.sharedManager.managedObjectModel) |
var error: NSError? |
|
let persistentStore = localCoordinator.addPersistentStoreWithType(NSSQLiteStoreType, configuration: nil, URL: CoreDataStackManager.sharedManager.storeURL, options: nil, error:&error) |
if persistentStore == nil { |
if outError != nil { |
outError.memory = error |
} |
return nil |
} |
|
let context = NSManagedObjectContext(concurrencyType: .PrivateQueueConcurrencyType) |
context.persistentStoreCoordinator = localCoordinator |
context.undoManager = nil |
|
return context |
} |