Skip to content

Instantly share code, notes, and snippets.

@keybuk
Last active March 15, 2016 04:22
Show Gist options
  • Save keybuk/17ab05caa19f21f583ce to your computer and use it in GitHub Desktop.
Save keybuk/17ab05caa19f21f583ce to your computer and use it in GitHub Desktop.
FetchedResultsController WIP
//
// FetchedResults.swift
// DungeonMaster
//
// Created by Scott James Remnant on 3/14/16.
// Copyright © 2016 Scott James Remnant. All rights reserved.
//
import CoreData
import Foundation
/// Provides a dynamic results set for a Core Data fetch request.
///
/// Initialize the instance with the desired `NSFetchRequest` and `NSManagedObjectContext` then call the `performFetch()` method to populate the `fetchedResults` list. Changes to the managed object context are then observed and the `fetchedResults` list updated automatically. An optional `handleChanges` block can be supplied which will be passed a list containing each change as they occur.
///
/// This is thus similar to `NSFetchedResultsController`, without the sectioning features of that class, and with the following new behavior for repeatedly calling `performFetch()`.
///
/// # Changes to the fetch request without notification
/// Changes to the fetch request, e.g. the `predicate` or `sortDescriptors` are permitted, however you must call `performFetch()` to refresh the set of `fetchedResults`, followed by `reloadData()` or equivalent on your view since the `handleChanges` block will not be called.
///
/// results.fetchRequest.sortDescriptors = ...
/// try! results.performFetch()
/// tableView.reloadData()
///
/// This is generally the most appropriate approach since refreshing the entire table is usually what you want for changes to a search pattern, sort criterion, etc.
///
/// # Changes to the fetch request _with notification_
/// In some circumstances, particularly changes to the result set as a result of transitioning between editing and non-editing, you may wish to handle the changes in a dynamic nature. To do this drop the call to `reloadData()` or equivalent, and instead call `performFetch(notifyChanges: true)`. The `handleChanges` block will be called with the list of changes that occured as a result of the new fetch.
///
/// results.fetchRequest.predicate = ...
/// try! results.performFetch(notifyChanges: true)
///
public class FetchedResults<Entity: NSManagedObject>: NSObject {
/// The fetch request used to do the fetching.
///
/// Changes to the fetch request are not automatically reflected in the fetched results. You must call `performFetch()` to refresh the fetched results.
public let fetchRequest: NSFetchRequest
/// The managed object context used to fetch objects.
///
/// The controller registers to listen to change notifications on this context in order to update the fetched results.
public let managedObjectContext: NSManagedObjectContext
/// Blocked called to handle hancles.
public let handleChanges: (([FetchedResultsChange<Entity>]) -> Void)?
public init(fetchRequest: NSFetchRequest, managedObjectContext: NSManagedObjectContext, handleChanges: (([FetchedResultsChange<Entity>]) -> Void)?) {
self.fetchRequest = fetchRequest
self.managedObjectContext = managedObjectContext
self.handleChanges = handleChanges
super.init()
NSNotificationCenter.defaultCenter().addObserver(self, selector: "managedObjectContextObjectsDidChange:", name: NSManagedObjectContextObjectsDidChangeNotification, object: self.managedObjectContext)
}
deinit {
NSNotificationCenter.defaultCenter().removeObserver(self)
}
/// The results of the fetch.
///
/// The value is `nil` until `performFetch()` is called.
public private(set) var fetchedObjects: [Entity]?
/// Cached entity used for the previous fetch.
private var entity: NSEntityDescription?
/// Cached predicate used for the previous fatch.
private var predicate: NSPredicate?
/// Cached sort descriptors used for the previous fetch.
private var sortDescriptors: [NSSortDescriptor]?
/// Cache of sort descriptor keys used for the previous fetch.
private var sortKeys: Set<String>?
/// Executes the fetch request.
///
/// This must be called after creating the controller to populate the initial `fetchedObjects`, and after making changes to the fetch request or external changes that would change the section of any object.
///
/// Normally changes to the fetched results caused by this method call will not result in the `handleChanges` block being called, and you should instead call a method such as `reloadData()` on the table or collection view to refresh the entire data set. This is almost always the most appropriate action since a large number of changes would be expected, including when changing a search predicate, sort order, etc.
///
/// Sometimes however you may want the changes "animated", for example transitioning between editing and non-editing modes, in that case pass `true` to `notifyChanges`. The changes in the fetched results will be analyzed and the `handleChanges` block called with a list of the changes found.
///
/// **Note:** this will not include any updates to the object values, since it assumed that merely changing the fetch request cannot change the values of the objects returned.
///
/// - parameter notifyChanges: when `true`, the changes to the fetched results set are analyzed and the `handleChanges` block called. Default is `false`.
public func performFetch(notifyChanges notifyChanges: Bool = false) throws {
let oldObjects = notifyChanges ? fetchedObjects : nil
fetchedObjects = try managedObjectContext.executeFetchRequest(fetchRequest) as? [Entity]
if let oldObjects = oldObjects, newObjects = fetchedObjects where notifyChanges {
notifyHandlerOfChanges(from: oldObjects, to: newObjects)
}
// Cache the entity, predicate and sort descriptor used for the fetch, so that intermediate changes to them don't cause strange change handler notifications before performFetch() is called.
entity = fetchRequest.entity
predicate = fetchRequest.predicate
sortDescriptors = fetchRequest.sortDescriptors
// Also make a specific cache of the set of keys used to sort the objects. For efficiency, we only re-sort when an update specifically changes these keys, and we may as well only incur the cost of creating the map once.
if let sortDescriptorKeys = fetchRequest.sortDescriptors?.flatMap({ $0.key?.componentsSeparatedByString(".").first }) {
sortKeys = Set(sortDescriptorKeys)
} else {
sortKeys = nil
}
}
/// Calls the `handleChanges` block with changes that occurred as a result of a re-fetch.
///
/// Most of the time this doesn't make sense, but when dealing with predicate/sort/section changes as a result of entering or leaving editing mode, this is perfect since the change handler will probably animate the changes along with the rest of the editing transitions.
///
/// - parameters from: the previous fetchedObjects contents
/// - parameters to: the new fetchedObjects contents
private func notifyHandlerOfChanges(from oldObjects: [Entity], to newObjects: [Entity]) {
guard let handleChanges = handleChanges else { return }
// To avoid O(n²) lookups, we build a map from object to index in the old and new results, which requires just one pass each over each result array (with a cheaper lookup in the second than `indexOf`), followed by a pass over the results.
var indexes: [ObjectIdentifier: (Array.Index?, Array.Index?, Entity)] = [:]
for (index, object) in oldObjects.enumerate() {
indexes[ObjectIdentifier(object)] = (index, nil, object)
}
// During the pass over the new results, we can count the number of insertions. Thus for any given index in the new results, we can know how many insertions have occurred up to, and including, that point.
var priorInserts: [Int] = []
for (index, object) in newObjects.enumerate() {
if let (oldIndex, _, _) = indexes[ObjectIdentifier(object)] {
indexes[ObjectIdentifier(object)] = (oldIndex, index, object)
priorInserts.append(priorInserts.last ?? 0)
} else {
indexes[ObjectIdentifier(object)] = (nil, index, object)
priorInserts.append((priorInserts.last ?? 0) + 1)
}
}
// Now we pass over the map itself, since both indexes are now known, we know whether a given object has been deleted, which means we can count the number of deletions, and thus we can reduce the erroneous moves.
var priorDeletes = 0
var changes: [FetchedResultsChange<Entity>] = []
for (index, newIndex, object) in indexes.values.sort({ $0.0 < $1.0 }) {
if let index = index, newIndex = newIndex {
// Comparing the old and new indexes will generate moves where there have been insertions and deletions. Since we counted those, we can generate an "adjusted" index, that would be the case where neither happened. If these adjusted indexes are the same, we don't need to generate a move event, it's just a movement as a result of the above.
let adjustedIndex = index + priorDeletes
let adjustedNewIndex = newIndex - priorInserts[newIndex]
if adjustedIndex != adjustedNewIndex {
// Moved.
changes.append(.Move(object: object, index: index, newIndex: newIndex))
}
} else if let index = index {
// Deleted.
changes.append(.Delete(object: object, index: index))
priorDeletes += 1
} else if let newIndex = newIndex {
// Inserted
changes.append(.Insert(object: object, newIndex: newIndex))
}
}
if changes.count > 0 {
handleChanges(changes)
}
}
/// Called on changes to the managed object context.
@objc private func managedObjectContextObjectsDidChange(notification: NSNotification) {
assert(notification.name == NSManagedObjectContextObjectsDidChangeNotification, "Notification method called for wrong notification.")
assert(notification.object === managedObjectContext, "Notification called for incorrect managed object context.")
// Rather than making the changes as we go, we keep track of them so that we can easily inform the change handler later.
var insertObjects: [Entity] = []
var removeObjects: [(Array.Index, Entity)] = []
var moveObjects: [(Array.Index, Entity)] = []
var updateObjects: [(Array.Index, Entity)] = []
// Process inserted objects.
// This is an easy case, if the object matches the predicate, we need to insert it.
if let insertedObjects = notification.userInfo?[NSInsertedObjectsKey] as? NSSet {
for case let insertedObject as Entity in insertedObjects {
guard insertedObject.entity === entity else { continue }
if let predicate = predicate {
guard predicate.evaluateWithObject(insertedObject) else { continue }
}
insertObjects.append(insertedObject)
}
}
// Process updated objects.
// This is the most complex case: where a predicate is in use, the update may cause the object to match or cease matching, and thus be treated as an insertion or removal; where sort keys are in use, the update may cause the object to move; otherwise it's an update only if it's in the results set.
if let updatedObjects = notification.userInfo?[NSUpdatedObjectsKey] as? NSSet {
for case let updatedObject as Entity in updatedObjects {
guard updatedObject.entity === entity else { continue }
if let index = fetchedObjects?.indexOf({ $0 === updatedObject }) {
if let predicate = predicate where !predicate.evaluateWithObject(updatedObject) {
// Object no longer matches the predicate.
removeObjects.append((index, updatedObject))
} else if let sortKeys = sortKeys where sortKeys.isSubsetOf(updatedObject.changedValuesForCurrentEvent().keys) {
// Object has moved within the sort order.
moveObjects.append((index, updatedObject))
} else {
// Object has changed in some other way.
updateObjects.append((index, updatedObject))
}
} else if let predicate = predicate where predicate.evaluateWithObject(updatedObject) {
// Object now matches the predicate.
insertObjects.append(updatedObject)
}
}
}
// Process deleted objects.
// Another pretty easy case, if the object is in our results set, we need to remove it.
if let deletedObjects = notification.userInfo?[NSDeletedObjectsKey] as? NSSet {
for case let deletedObject as Entity in deletedObjects {
guard deletedObject.entity === entity else { continue }
if let index = fetchedObjects?.indexOf({ $0 === deletedObject }) {
removeObjects.append((index, deletedObject))
}
}
}
if let refreshedObjects = notification.userInfo?[NSRefreshedObjectsKey] as? NSSet {
}
if let invalidatedObjects = notification.userInfo?[NSInvalidatedObjectsKey] as? NSSet {
}
if let _ = notification.userInfo?[NSInvalidatedAllObjectsKey] {
}
// Update the fetchedObjects list.
guard insertObjects.count > 0 || removeObjects.count > 0 || moveObjects.count > 0 || updateObjects.count > 0 else { return }
// First we remove deleted objects. We do this by index in reverse order so that each index is still valid.
for (index, _) in removeObjects.sort({ $1.0 < $0.0 }) {
fetchedObjects!.removeAtIndex(index)
}
// Next we append the new objects.
fetchedObjects!.appendContentsOf(insertObjects)
// Finally we re-sort the set of objects. For efficiency we only do this when objects have been inserted, and thus need to be sorted into position, or if the "sort keys" of an object have changed.
if let sortDescriptors = sortDescriptors where insertObjects.count > 0 || moveObjects.count > 0 {
fetchedObjects = (fetchedObjects! as NSArray).sortedArrayUsingDescriptors(sortDescriptors) as? [Entity]
}
// The order of these notifications doesn't matter, since `index` is always according to the previous copy of `fetchedObjects`, and `newIndex` is always according to the fully updated copy.
guard let handleChanges = handleChanges else { return }
var changes: [FetchedResultsChange<Entity>] = []
for object in insertObjects {
let newIndex = fetchedObjects!.indexOf({ $0 === object })!
changes.append(.Insert(object: object, newIndex: newIndex))
}
for (index, object) in removeObjects {
changes.append(.Delete(object: object, index: index))
}
for (index, object) in moveObjects {
let newIndex = fetchedObjects!.indexOf({ $0 === object })!
changes.append(.Move(object: object, index: index, newIndex: newIndex))
}
for (index, object) in updateObjects {
changes.append(.Update(object: object, index: index))
}
handleChanges(changes)
}
}
/// Types of changes to the fetched results reported to the `changeHandler`.
///
/// - `Insert`: `object` was inserted at `newIndex`.
/// - `Delete`: `object` at `index` was deleted.
/// - `Move`: the object `object` was moved from `index` to `newIndex`.
/// - `Update`: the object `object` at `index` was updated.
public enum FetchedResultsChange<Entity: NSManagedObject> {
case Insert(object: Entity, newIndex: Int)
case Delete(object: Entity, index: Int)
case Move(object: Entity, index: Int, newIndex: Int)
case Update(object: Entity, index: Int)
}
//
// FetchedResultsTests.swift
// DungeonMaster
//
// Created by Scott James Remnant on 3/14/16.
// Copyright © 2016 Scott James Remnant. All rights reserved.
//
import CoreData
import XCTest
@testable import DungeonMaster
class FetchedResultsTests: XCTestCase {
let sampleData = [
( "Scott", 35, 0 ),
( "Shane", 29, 0 ),
( "Tague", 43, 0 ),
( "Jinger", 30, 1 ),
( "Sam", 34, 1 ),
( "Caitlin", 13, 1 )
]
var sampleObjects: [String:NSManagedObject] = [:]
lazy var managedObjectModel: NSManagedObjectModel = {
let model = NSManagedObjectModel()
let person = NSEntityDescription()
person.name = "Person"
let nameAttribute = NSAttributeDescription()
nameAttribute.name = "name"
nameAttribute.attributeType = .StringAttributeType
person.properties.append(nameAttribute)
let ageAttribute = NSAttributeDescription()
ageAttribute.name = "age"
ageAttribute.attributeType = .Integer16AttributeType
person.properties.append(ageAttribute)
let sexAttribute = NSAttributeDescription()
sexAttribute.name = "sex"
sexAttribute.attributeType = .Integer16AttributeType
person.properties.append(sexAttribute)
model.entities.append(person)
return model
}()
var managedObjectContext: NSManagedObjectContext!
func makePerson(name name: String, age: Int, sex: Int) -> NSManagedObject {
let personEntity = NSEntityDescription.entityForName("Person", inManagedObjectContext: managedObjectContext)!
let person = NSManagedObject(entity: personEntity, insertIntoManagedObjectContext: managedObjectContext)
person.setValue(name, forKey: "name")
person.setValue(age, forKey: "age")
person.setValue(sex, forKey: "sex")
return person
}
override func setUp() {
super.setUp()
// Set up the in-memory store.
let persistentStoreCoordinator = NSPersistentStoreCoordinator(managedObjectModel: self.managedObjectModel)
try! persistentStoreCoordinator.addPersistentStoreWithType(NSInMemoryStoreType, configuration: nil, URL: nil, options: nil)
managedObjectContext = NSManagedObjectContext(concurrencyType: .MainQueueConcurrencyType)
managedObjectContext.persistentStoreCoordinator = persistentStoreCoordinator
// Set up the sample data.
sampleObjects = [:]
for (name, age, sex) in sampleData {
let person = makePerson(name: name, age: age, sex: sex)
sampleObjects[name] = person
}
try! managedObjectContext.save()
}
override func tearDown() {
// Put teardown code here. This method is called after the invocation of each test method in the class.
super.tearDown()
}
func testSimpleRequest() {
// Make a simple fetch request on all entities and make sure they are all returned.
let fetchRequest = NSFetchRequest(entityName: "Person")
let results = FetchedResults(fetchRequest: fetchRequest, managedObjectContext: managedObjectContext, handleChanges: nil)
try! results.performFetch()
XCTAssertNotNil(results.fetchedObjects, "Expected fetched objects after performing fetch.")
XCTAssertEqual(results.fetchedObjects!.count, 6, "Expected all sample objects to be fetched.")
let expectedNames = Set(sampleData.map({ $0.0 }))
var fetchedNames = Set(results.fetchedObjects!.map({ $0.valueForKey("name") as! String }))
XCTAssertEqual(expectedNames, fetchedNames, "Set of fetched objects was incorrect.")
// Insert a new person. This should be observed by the controller, and result in a change to the fetched objects array.
makePerson(name: "Ryan", age: 31, sex: 0)
try! managedObjectContext.save()
XCTAssertNotNil(results.fetchedObjects, "Fetched objects unexpectedly turned into nil.")
XCTAssertEqual(results.fetchedObjects!.count, 7, "Expected the object count to have increased.")
fetchedNames = Set(results.fetchedObjects!.map({ $0.valueForKey("name") as! String }))
XCTAssertEqual(fetchedNames.subtract(expectedNames), [ "Ryan" ], "Expected the new object to be the one inserted.")
// Delete an existing person. This should be observed by the controller, and result in a change to the fetched objects array.
managedObjectContext.deleteObject(sampleObjects["Sam"]!)
try! managedObjectContext.save()
XCTAssertNotNil(results.fetchedObjects, "Fetched objects unexpectedly turned into nil.")
XCTAssertEqual(results.fetchedObjects!.count, 6, "Expected the object count to have decreased.")
fetchedNames = Set(results.fetchedObjects!.map({ $0.valueForKey("name") as! String }))
XCTAssertEqual(expectedNames.subtract(fetchedNames), [ "Sam" ], "Expected the deleted object to be missing.")
XCTAssertEqual(fetchedNames.subtract(expectedNames), [ "Ryan" ], "Expected the previously inserted object to be present.")
}
func testRequestWithPredicate() {
// Make a fetch request on entities with a filter predicate, and make sure the expected set is returned.
let fetchRequest = NSFetchRequest(entityName: "Person")
fetchRequest.predicate = NSPredicate(format: "age > 30")
let results = FetchedResults(fetchRequest: fetchRequest, managedObjectContext: managedObjectContext, handleChanges: nil)
try! results.performFetch()
XCTAssertNotNil(results.fetchedObjects, "Expected fetched objects after performing fetch.")
XCTAssertEqual(results.fetchedObjects!.count, 3, "Expected a subset of sample objects to be fetched.")
let expectedNames = Set(sampleData.filter({ $0.1 > 30 }).map({ $0.0 }))
var fetchedNames = Set(results.fetchedObjects!.map({ $0.valueForKey("name") as! String }))
XCTAssertEqual(expectedNames, fetchedNames, "Set of fetched objects was incorrect.")
// Insert a person that matches the predicate. This should be observed by the controller, and result in a change to the fetched objects array.
makePerson(name: "Ryan", age: 31, sex: 0)
try! managedObjectContext.save()
XCTAssertNotNil(results.fetchedObjects, "Fetched objects unexpectedly turned into nil.")
XCTAssertEqual(results.fetchedObjects!.count, 4, "Expected the object count to have increased.")
fetchedNames = Set(results.fetchedObjects!.map({ $0.valueForKey("name") as! String }))
XCTAssertEqual(fetchedNames.subtract(expectedNames), [ "Ryan" ], "Expected the new object to be the one inserted.")
// Insert a person that doesn't match the predicate. While this should be observed by the controller, it should not result in any changes.
makePerson(name: "Michael", age: 30, sex: 0)
try! managedObjectContext.save()
XCTAssertNotNil(results.fetchedObjects, "Fetched objects unexpectedly turned into nil.")
XCTAssertEqual(results.fetchedObjects!.count, 4, "Expected the object count to have remained the same.")
fetchedNames = Set(results.fetchedObjects!.map({ $0.valueForKey("name") as! String }))
XCTAssertEqual(fetchedNames.subtract(expectedNames), [ "Ryan" ], "Expected only the previous object to be the one inserted.")
// Remove a person matching the predicate. This should be observed by the controller, and result in a change to the fetched objects array.
managedObjectContext.deleteObject(sampleObjects["Sam"]!)
try! managedObjectContext.save()
XCTAssertNotNil(results.fetchedObjects, "Fetched objects unexpectedly turned into nil.")
XCTAssertEqual(results.fetchedObjects!.count, 3, "Expected the object count to have decreased.")
fetchedNames = Set(results.fetchedObjects!.map({ $0.valueForKey("name") as! String }))
XCTAssertEqual(expectedNames.subtract(fetchedNames), [ "Sam" ], "Expected the deleted object to be missing.")
XCTAssertEqual(fetchedNames.subtract(expectedNames), [ "Ryan" ], "Expected the previously inserted object to be present.")
// Remove a person that doesn't match the predicate. While this should be observed by the controller, it should not result in any changes.
managedObjectContext.deleteObject(sampleObjects["Caitlin"]!)
try! managedObjectContext.save()
XCTAssertNotNil(results.fetchedObjects, "Fetched objects unexpectedly turned into nil.")
XCTAssertEqual(results.fetchedObjects!.count, 3, "Expected the object count to have remained the same.")
fetchedNames = Set(results.fetchedObjects!.map({ $0.valueForKey("name") as! String }))
XCTAssertEqual(expectedNames.subtract(fetchedNames), [ "Sam" ], "Expected the previously deleted object to be missing.")
XCTAssertEqual(fetchedNames.subtract(expectedNames), [ "Ryan" ], "Expected the previously inserted object to be present.")
// Update a person to make it now match the predicate.
sampleObjects["Jinger"]?.setValue(31, forKey: "age")
try! managedObjectContext.save()
XCTAssertNotNil(results.fetchedObjects, "Fetched objects unexpectedly turned into nil.")
XCTAssertEqual(results.fetchedObjects!.count, 4, "Expected the object count to have increased.")
fetchedNames = Set(results.fetchedObjects!.map({ $0.valueForKey("name") as! String }))
XCTAssertEqual(expectedNames.subtract(fetchedNames), [ "Sam" ], "Expected the previously deleted object to be missing.")
XCTAssertEqual(fetchedNames.subtract(expectedNames), Set([ "Jinger", "Ryan" ]), "Expected the inserted and updated objects to be present.")
// Update a person to stop it matching the predicate.
sampleObjects["Tague"]?.setValue(21, forKey: "age")
try! managedObjectContext.save()
XCTAssertNotNil(results.fetchedObjects, "Fetched objects unexpectedly turned into nil.")
XCTAssertEqual(results.fetchedObjects!.count, 3, "Expected the object count to have decreated.")
fetchedNames = Set(results.fetchedObjects!.map({ $0.valueForKey("name") as! String }))
XCTAssertEqual(expectedNames.subtract(fetchedNames), Set([ "Sam", "Tague" ]), "Expected the previously deleted and most recently updated objects to be missing.")
XCTAssertEqual(fetchedNames.subtract(expectedNames), Set([ "Jinger", "Ryan" ]), "Expected the inserted and previously updated objects to be present.")
}
func testRequestWithSortDescriptors() {
// Make a fetch request on entities with a sort descriptors, and make sure the expected is returned in the expected order.
let fetchRequest = NSFetchRequest(entityName: "Person")
fetchRequest.sortDescriptors = [ NSSortDescriptor(key: "age", ascending: true) ]
let results = FetchedResults(fetchRequest: fetchRequest, managedObjectContext: managedObjectContext, handleChanges: nil)
try! results.performFetch()
XCTAssertNotNil(results.fetchedObjects, "Expected fetched objects after performing fetch.")
XCTAssertEqual(results.fetchedObjects!.count, 6, "Expected all sample objects to be fetched.")
var expectedNames = sampleData.sort({ $0.1 < $1.1 }).map({ $0.0 })
var fetchedNames = results.fetchedObjects!.map({ $0.valueForKey("name") as! String })
XCTAssertEqual(expectedNames, fetchedNames, "List of fetched objects was incorrect.")
// Insert a person, and make sure they go into the correct place in the sort order.
makePerson(name: "Ryan", age: 31, sex: 0)
try! managedObjectContext.save()
XCTAssertNotNil(results.fetchedObjects, "Fetched objects unexpectedly turned into nil.")
XCTAssertEqual(results.fetchedObjects!.count, 7, "Expected the object count to have increased.")
expectedNames.insert("Ryan", atIndex: 3)
fetchedNames = results.fetchedObjects!.map({ $0.valueForKey("name") as! String })
XCTAssertEqual(expectedNames, fetchedNames, "Expected the inserted object to be in the right place in the fetch request.")
// Update a person in the request to move them in the sort order.
sampleObjects["Tague"]?.setValue(21, forKey: "age")
try! managedObjectContext.save()
XCTAssertNotNil(results.fetchedObjects, "Fetched objects unexpectedly turned into nil.")
XCTAssertEqual(results.fetchedObjects!.count, 7, "Expected the object count to have remained the same.")
expectedNames.insert(expectedNames.removeAtIndex(6), atIndex: 1)
fetchedNames = results.fetchedObjects!.map({ $0.valueForKey("name") as! String })
XCTAssertEqual(expectedNames, fetchedNames, "Expected the updated object to have moved in the fetch request.")
}
func testInsertWithBlock() {
// Make a fetch request with a block to receive changes.
let fetchRequest = NSFetchRequest(entityName: "Person")
var changes: [FetchedResultsChange<NSManagedObject>] = []
let results = FetchedResults(fetchRequest: fetchRequest, managedObjectContext: managedObjectContext, handleChanges: { changes.appendContentsOf($0) })
try! results.performFetch()
XCTAssertNotNil(results.fetchedObjects, "Expected fetched objects after performing fetch.")
XCTAssertEqual(results.fetchedObjects!.count, 6, "Expected all sample objects to be fetched.")
XCTAssertEqual(changes.count, 0, "Expected no changes.")
// Insert a new person. This should be observed by the controller.
let insertedObject = makePerson(name: "Ryan", age: 31, sex: 0)
try! managedObjectContext.save()
XCTAssertEqual(changes.count, 1, "Wrong number of changes.")
switch changes.removeFirst() {
case .Insert(object: insertedObject, newIndex: 6):
break
default:
XCTFail("Expected insert of the new object.")
}
}
func testInsertWithBlockAndSort() {
// Make a sorted fetch request with a block to receive changes.
let fetchRequest = NSFetchRequest(entityName: "Person")
fetchRequest.sortDescriptors = [ NSSortDescriptor(key: "age", ascending: true) ]
var changes: [FetchedResultsChange<NSManagedObject>] = []
let results = FetchedResults(fetchRequest: fetchRequest, managedObjectContext: managedObjectContext, handleChanges: { changes.appendContentsOf($0) })
try! results.performFetch()
XCTAssertNotNil(results.fetchedObjects, "Expected fetched objects after performing fetch.")
XCTAssertEqual(results.fetchedObjects!.count, 6, "Expected all sample objects to be fetched.")
XCTAssertEqual(changes.count, 0, "Expected no changes.")
// Insert a new person. This should be observed by the controller.
let insertedObject = makePerson(name: "Ryan", age: 31, sex: 0)
try! managedObjectContext.save()
XCTAssertEqual(changes.count, 1, "Wrong number of changes.")
switch changes.removeFirst() {
case .Insert(object: insertedObject, newIndex: 3):
break
default:
XCTFail("Expected insert of the new object.")
}
}
func testDeleteWithBlock() {
// Make a fetch request with a block to receive changes. Use sort descriptors to make the indexes consistent.
let fetchRequest = NSFetchRequest(entityName: "Person")
fetchRequest.sortDescriptors = [ NSSortDescriptor(key: "age", ascending: true) ]
var changes: [FetchedResultsChange<NSManagedObject>] = []
let results = FetchedResults(fetchRequest: fetchRequest, managedObjectContext: managedObjectContext, handleChanges: { changes.appendContentsOf($0) })
try! results.performFetch()
XCTAssertNotNil(results.fetchedObjects, "Expected fetched objects after performing fetch.")
XCTAssertEqual(results.fetchedObjects!.count, 6, "Expected all sample objects to be fetched.")
XCTAssertEqual(changes.count, 0, "Expected no changes.")
// Remove a person. This should be observed by the controller.
managedObjectContext.deleteObject(sampleObjects["Caitlin"]!)
try! managedObjectContext.save()
XCTAssertEqual(changes.count, 1, "Wrong number of changes.")
switch changes.removeFirst() {
case .Delete(object: sampleObjects["Caitlin"]!, index: 0):
break
default:
XCTFail("Expected delete of the object.")
}
}
func testMoveWithBlock() {
// Make a fetch request with a block to receive changes.. Use sort descriptors to make the indexes consistent.
let fetchRequest = NSFetchRequest(entityName: "Person")
fetchRequest.sortDescriptors = [ NSSortDescriptor(key: "age", ascending: true) ]
var changes: [FetchedResultsChange<NSManagedObject>] = []
let results = FetchedResults(fetchRequest: fetchRequest, managedObjectContext: managedObjectContext, handleChanges: { changes.appendContentsOf($0) })
try! results.performFetch()
XCTAssertNotNil(results.fetchedObjects, "Expected fetched objects after performing fetch.")
XCTAssertEqual(results.fetchedObjects!.count, 6, "Expected all sample objects to be fetched.")
XCTAssertEqual(changes.count, 0, "Expected no changes.")
// Update a person's age (the sort key). This should be observed by the controller.
sampleObjects["Tague"]?.setValue(21, forKey: "age")
try! managedObjectContext.save()
XCTAssertEqual(changes.count, 1, "Wrong number of changes.")
switch changes.removeFirst() {
case .Move(object: sampleObjects["Tague"]!, index: 5, newIndex: 1):
break
default:
XCTFail("Expected move of the object.")
}
}
func testUpdateWithBlock() {
// Make a fetch request with a block to receive changes.. Use sort descriptors to make the indexes consistent.
let fetchRequest = NSFetchRequest(entityName: "Person")
fetchRequest.sortDescriptors = [ NSSortDescriptor(key: "age", ascending: true) ]
var changes: [FetchedResultsChange<NSManagedObject>] = []
let results = FetchedResults(fetchRequest: fetchRequest, managedObjectContext: managedObjectContext, handleChanges: { changes.appendContentsOf($0) })
try! results.performFetch()
XCTAssertNotNil(results.fetchedObjects, "Expected fetched objects after performing fetch.")
XCTAssertEqual(results.fetchedObjects!.count, 6, "Expected all sample objects to be fetched.")
XCTAssertEqual(changes.count, 0, "Expected no changes.")
// Update a person's sex. This should be observed by the controller.
sampleObjects["Sam"]?.setValue(0, forKey: "sex")
try! managedObjectContext.save()
XCTAssertEqual(changes.count, 1, "Wrong number of changes.")
switch changes.removeFirst() {
case .Update(object: sampleObjects["Sam"]!, index: 3):
break
default:
XCTFail("Expected update of the object.")
}
}
func testUpdateAndMoveWithBlock() {
// Make a fetch request with a block to receive changes.. Use sort descriptors to make the indexes consistent.
let fetchRequest = NSFetchRequest(entityName: "Person")
fetchRequest.sortDescriptors = [ NSSortDescriptor(key: "age", ascending: true) ]
var changes: [FetchedResultsChange<NSManagedObject>] = []
let results = FetchedResults(fetchRequest: fetchRequest, managedObjectContext: managedObjectContext, handleChanges: { changes.appendContentsOf($0) })
try! results.performFetch()
XCTAssertNotNil(results.fetchedObjects, "Expected fetched objects after performing fetch.")
XCTAssertEqual(results.fetchedObjects!.count, 6, "Expected all sample objects to be fetched.")
XCTAssertEqual(changes.count, 0, "Expected no changes.")
// Update a person's age and sex. This should be observed by the controller, and result in a single move change.
sampleObjects["Sam"]?.setValue(55, forKey: "age")
sampleObjects["Sam"]?.setValue(0, forKey: "sex")
try! managedObjectContext.save()
XCTAssertEqual(changes.count, 1, "Wrong number of changes.")
switch changes.removeFirst() {
case .Move(object: sampleObjects["Sam"]!, index: 3, newIndex: 5):
break
default:
XCTFail("Expected move of the object.")
}
}
func testMultipleInserts() {
// Make a sorted fetch request with a block to receive changes.
let fetchRequest = NSFetchRequest(entityName: "Person")
fetchRequest.sortDescriptors = [ NSSortDescriptor(key: "age", ascending: true) ]
var changes: [FetchedResultsChange<NSManagedObject>] = []
let results = FetchedResults(fetchRequest: fetchRequest, managedObjectContext: managedObjectContext, handleChanges: { changes.appendContentsOf($0) })
try! results.performFetch()
XCTAssertNotNil(results.fetchedObjects, "Expected fetched objects after performing fetch.")
XCTAssertEqual(results.fetchedObjects!.count, 6, "Expected all sample objects to be fetched.")
XCTAssertEqual(changes.count, 0, "Expected no changes.")
// Insert two new people. Both changes should be batched.
let insertedObject1 = makePerson(name: "Ryan", age: 31, sex: 0)
let insertedObject2 = makePerson(name: "Aaron", age: 32, sex: 0)
try! managedObjectContext.save()
XCTAssertEqual(changes.count, 2, "Wrong number of changes.")
for _ in 0..<2 {
switch changes.removeFirst() {
case .Insert(object: insertedObject1, newIndex: 3):
break
case .Insert(object: insertedObject2, newIndex: 4):
break
default:
XCTFail("Expected insert of one of the objects.")
}
}
// Make sure the set of fetched objects is correct.
let expectedNames = Set(sampleData.map({ $0.0 }))
let fetchedNames = Set(results.fetchedObjects!.map({ $0.valueForKey("name") as! String }))
XCTAssertNotNil(results.fetchedObjects, "Fetched objects unexpectedly turned into nil.")
XCTAssertEqual(results.fetchedObjects!.count, 8, "Expected the object count to have increased.")
XCTAssertEqual(fetchedNames.subtract(expectedNames), Set([ "Ryan", "Aaron" ]), "Expected the inserted objects to be extra.")
}
func testMultipleDeletes() {
// Make a sorted fetch request with a block to receive changes.
let fetchRequest = NSFetchRequest(entityName: "Person")
fetchRequest.sortDescriptors = [ NSSortDescriptor(key: "age", ascending: true) ]
var changes: [FetchedResultsChange<NSManagedObject>] = []
let results = FetchedResults(fetchRequest: fetchRequest, managedObjectContext: managedObjectContext, handleChanges: { changes.appendContentsOf($0) })
try! results.performFetch()
XCTAssertNotNil(results.fetchedObjects, "Expected fetched objects after performing fetch.")
XCTAssertEqual(results.fetchedObjects!.count, 6, "Expected all sample objects to be fetched.")
XCTAssertEqual(changes.count, 0, "Expected no changes.")
// Remove two people. Both changes should be batched, and the index of the second should be consistent with its original position in the results, and not updated relative to the previous index.
managedObjectContext.deleteObject(sampleObjects["Caitlin"]!)
managedObjectContext.deleteObject(sampleObjects["Sam"]!)
try! managedObjectContext.save()
XCTAssertEqual(changes.count, 2, "Wrong number of changes.")
for _ in 0..<2 {
switch changes.removeFirst() {
case .Delete(object: sampleObjects["Caitlin"]!, index: 0):
break
case .Delete(object: sampleObjects["Sam"]!, index: 3):
break
default:
XCTFail("Expected delete of one of the objects.")
}
}
// Make sure the set of fetched objects is correct.
let expectedNames = Set(sampleData.map({ $0.0 }))
let fetchedNames = Set(results.fetchedObjects!.map({ $0.valueForKey("name") as! String }))
XCTAssertNotNil(results.fetchedObjects, "Fetched objects unexpectedly turned into nil.")
XCTAssertEqual(results.fetchedObjects!.count, 4, "Expected the object count to have decreated.")
XCTAssertEqual(expectedNames.subtract(fetchedNames), Set([ "Caitlin", "Sam" ]), "Expected the deleted objects to be missing.")
}
func testMultipleMoves() {
// Make a sorted fetch request with a block to receive changes.
let fetchRequest = NSFetchRequest(entityName: "Person")
fetchRequest.sortDescriptors = [ NSSortDescriptor(key: "age", ascending: true) ]
var changes: [FetchedResultsChange<NSManagedObject>] = []
let results = FetchedResults(fetchRequest: fetchRequest, managedObjectContext: managedObjectContext, handleChanges: { changes.appendContentsOf($0) })
try! results.performFetch()
XCTAssertNotNil(results.fetchedObjects, "Expected fetched objects after performing fetch.")
XCTAssertEqual(results.fetchedObjects!.count, 6, "Expected all sample objects to be fetched.")
XCTAssertEqual(changes.count, 0, "Expected no changes.")
// Move two people. Both changes should be batched, with the second index reflective simply of its final position in the list, without adjusting for ordering.
sampleObjects["Tague"]?.setValue(21, forKey: "age")
sampleObjects["Scott"]?.setValue(20, forKey: "age")
try! managedObjectContext.save()
XCTAssertEqual(changes.count, 2, "Wrong number of delegate events.")
for _ in 0..<2 {
switch changes.removeFirst() {
case .Move(object: sampleObjects["Tague"]!, index: 5, newIndex: 2):
break
case .Move(object: sampleObjects["Scott"]!, index: 4, newIndex: 1):
break
default:
XCTFail("Expected move of one of the objects.")
}
}
}
func testMoveToSamePlace() {
// Make a sorted fetch request with a block to receive changes.
let fetchRequest = NSFetchRequest(entityName: "Person")
fetchRequest.sortDescriptors = [ NSSortDescriptor(key: "age", ascending: true) ]
var changes: [FetchedResultsChange<NSManagedObject>] = []
let results = FetchedResults(fetchRequest: fetchRequest, managedObjectContext: managedObjectContext, handleChanges: { changes.appendContentsOf($0) })
try! results.performFetch()
XCTAssertNotNil(results.fetchedObjects, "Expected fetched objects after performing fetch.")
XCTAssertEqual(results.fetchedObjects!.count, 6, "Expected all sample objects to be fetched.")
XCTAssertEqual(changes.count, 0, "Expected no changes.")
// Change one person's age, which shouldn't actually change their position in the order; we test this because NSFRC does generate a delegate event for this, and we want to be compatible.
sampleObjects["Sam"]?.setValue(33, forKey: "age")
try! managedObjectContext.save()
XCTAssertEqual(changes.count, 1, "Wrong number of changes.")
switch changes.removeFirst() {
case .Move(object: sampleObjects["Sam"]!, index: 3, newIndex: 3):
break
default:
XCTFail("Expected move of the object to its same position.")
}
}
func testChangePredicateWithoutBlock() {
// Make a fetch request with a predicate, also use a sort to make the test more consistent.
let fetchRequest = NSFetchRequest(entityName: "Person")
fetchRequest.predicate = NSPredicate(format: "age > 30")
fetchRequest.sortDescriptors = [ NSSortDescriptor(key: "name", ascending: true) ]
var changes: [FetchedResultsChange<NSManagedObject>] = []
let results = FetchedResults(fetchRequest: fetchRequest, managedObjectContext: managedObjectContext, handleChanges: { changes.appendContentsOf($0) })
try! results.performFetch()
XCTAssertNotNil(results.fetchedObjects, "Expected fetched objects after performing fetch.")
XCTAssertEqual(results.fetchedObjects!.count, 3, "Expected a subset of sample objects to be fetched.")
XCTAssertEqual(changes.count, 0, "Expected no changes.")
var expectedNames = sampleData.filter({ $0.1 > 30 }).map({ $0.0 }).sort()
var fetchedNames = results.fetchedObjects!.map({ $0.valueForKey("name") as! String })
XCTAssertEqual(expectedNames, fetchedNames, "List of fetched objects was incorrect.")
// Now we change the predicate in the fetch request, and call performFetch() again. We make sure that the fetched results are consistent with the change.
fetchRequest.predicate = NSPredicate(format: "age < 40")
try! results.performFetch()
expectedNames = sampleData.filter({ $0.1 < 40 }).map({ $0.0 }).sort()
fetchedNames = results.fetchedObjects!.map({ $0.valueForKey("name") as! String })
XCTAssertNotNil(results.fetchedObjects, "Expected fetched objects after re-performing fetch.")
XCTAssertEqual(results.fetchedObjects!.count, 5, "Expected a larger subset of sample objects to be fetched.")
XCTAssertEqual(expectedNames, fetchedNames, "Fetched results after predicate change was incorrect.")
// No changes should have been notified
XCTAssertEqual(changes.count, 0, "Expected no changes.")
}
func testChangePredicateWithBlock() {
// Make a fetch request with a predicate and with a block to receive changes. Also use a sort to make the test more consistent.
let fetchRequest = NSFetchRequest(entityName: "Person")
fetchRequest.predicate = NSPredicate(format: "age > 30")
fetchRequest.sortDescriptors = [ NSSortDescriptor(key: "name", ascending: true) ]
var changes: [FetchedResultsChange<NSManagedObject>] = []
let results = FetchedResults(fetchRequest: fetchRequest, managedObjectContext: managedObjectContext, handleChanges: { changes.appendContentsOf($0) })
try! results.performFetch()
XCTAssertNotNil(results.fetchedObjects, "Expected fetched objects after performing fetch.")
XCTAssertEqual(results.fetchedObjects!.count, 3, "Expected a subset of sample objects to be fetched.")
XCTAssertEqual(changes.count, 0, "Expected no changes.")
var expectedNames = sampleData.filter({ $0.1 > 30 }).map({ $0.0 }).sort()
var fetchedNames = results.fetchedObjects!.map({ $0.valueForKey("name") as! String })
XCTAssertEqual(expectedNames, fetchedNames, "List of fetched objects was incorrect.")
// Now we change the predicate in the fetch request, and call performFetch() again, this time with notifications enabled. We make sure that the fetched results are consistent with the change, and that the changes were passed to the block.
fetchRequest.predicate = NSPredicate(format: "age < 40")
try! results.performFetch(notifyChanges: true)
expectedNames = sampleData.filter({ $0.1 < 40 }).map({ $0.0 }).sort()
fetchedNames = results.fetchedObjects!.map({ $0.valueForKey("name") as! String })
XCTAssertNotNil(results.fetchedObjects, "Expected fetched objects after re-performing fetch.")
XCTAssertEqual(results.fetchedObjects!.count, 5, "Expected a larger subset of sample objects to be fetched.")
XCTAssertEqual(expectedNames, fetchedNames, "Fetched results after predicate change was incorrect.")
XCTAssertEqual(changes.count, 4, "Wrong number of changes.")
for _ in 0..<4 {
switch changes.removeFirst() {
case .Insert(object: sampleObjects["Caitlin"]!, newIndex: 0):
break
case .Insert(object: sampleObjects["Shane"]!, newIndex: 4):
break
case .Insert(object: sampleObjects["Jinger"]!, newIndex: 1):
break
case .Delete(object: sampleObjects["Tague"]!, index: 2):
break
default:
XCTFail("Expected insert of new object or delete of old.")
}
}
}
func testChangeSortWithBlock() {
// Make a fetch request with an initial sort criteria and with a block to receive changes.
let fetchRequest = NSFetchRequest(entityName: "Person")
fetchRequest.sortDescriptors = [ NSSortDescriptor(key: "age", ascending: true) ]
var changes: [FetchedResultsChange<NSManagedObject>] = []
let results = FetchedResults(fetchRequest: fetchRequest, managedObjectContext: managedObjectContext, handleChanges: { changes.appendContentsOf($0) })
try! results.performFetch()
XCTAssertNotNil(results.fetchedObjects, "Expected fetched objects after performing fetch.")
XCTAssertEqual(results.fetchedObjects!.count, 6, "Expected all sample objects to be fetched.")
XCTAssertEqual(changes.count, 0, "Expected no changes.")
var expectedNames = sampleData.sort({ $0.1 < $1.1 }).map({ $0.0 })
var fetchedNames = results.fetchedObjects!.map({ $0.valueForKey("name") as! String })
XCTAssertEqual(expectedNames, fetchedNames, "List of fetched objects was incorrect.")
// Now change the sort criteria, and call performFetch() again with notifications enabled.
fetchRequest.sortDescriptors = [ NSSortDescriptor(key: "sex", ascending: true), NSSortDescriptor(key: "age", ascending: true) ]
try! results.performFetch(notifyChanges: true)
let sortedNames = sampleData.sort({ $0.2 < $1.2 || ($0.2 == $1.2 && $0.1 < $1.1) })
expectedNames = sortedNames.map({ $0.0 })
fetchedNames = results.fetchedObjects!.map({ $0.valueForKey("name") as! String })
XCTAssertNotNil(results.fetchedObjects, "Expected fetched objects after re-performing fetch.")
XCTAssertEqual(results.fetchedObjects!.count, 6, "Expected the same set of sample objects to be fetched.")
XCTAssertEqual(expectedNames, fetchedNames, "Fetched results after predicate change was incorrect.")
XCTAssertEqual(changes.count, 6, "Wrong number of changes.")
for _ in 0..<6 {
switch changes.removeFirst() {
case .Move(object: sampleObjects["Caitlin"]!, index: 0, newIndex: 3):
break
case .Move(object: sampleObjects["Shane"]!, index: 1, newIndex: 0):
break
case .Move(object: sampleObjects["Jinger"]!, index: 2, newIndex: 4):
break
case .Move(object: sampleObjects["Sam"]!, index: 3, newIndex: 5):
break
case .Move(object: sampleObjects["Scott"]!, index: 4, newIndex: 1):
break
case .Move(object: sampleObjects["Tague"]!, index: 5, newIndex: 2):
break
default:
XCTFail("Expected move of one of the objects.")
}
}
}
func testChangePredicateAndSortWithBlock() {
// Make a fetch request with an initial predicate and sort criteria, and with a block to receive changes.
let fetchRequest = NSFetchRequest(entityName: "Person")
fetchRequest.predicate = NSPredicate(format: "age > 30")
fetchRequest.sortDescriptors = [ NSSortDescriptor(key: "age", ascending: true) ]
var changes: [FetchedResultsChange<NSManagedObject>] = []
let results = FetchedResults(fetchRequest: fetchRequest, managedObjectContext: managedObjectContext, handleChanges: { changes.appendContentsOf($0) })
try! results.performFetch()
XCTAssertNotNil(results.fetchedObjects, "Expected fetched objects after performing fetch.")
XCTAssertEqual(results.fetchedObjects!.count, 3, "Expected a subset of sample objects to be fetched.")
XCTAssertEqual(changes.count, 0, "Expected no changes.")
var expectedNames = sampleData.filter({ $0.1 > 30 }).sort({ $0.1 < $1.1 }).map({ $0.0 })
var fetchedNames = results.fetchedObjects!.map({ $0.valueForKey("name") as! String })
XCTAssertEqual(expectedNames, fetchedNames, "List of fetched objects was incorrect.")
// Now change the predicate and sort criteria, and call performFetch() again with notifications enabled.
fetchRequest.predicate = NSPredicate(format: "age < 40")
fetchRequest.sortDescriptors = [ NSSortDescriptor(key: "sex", ascending: true), NSSortDescriptor(key: "age", ascending: true) ]
try! results.performFetch(notifyChanges: true)
let sortedNames = sampleData.filter({ $0.1 < 40 }).sort({ $0.2 < $1.2 || ($0.2 == $1.2 && $0.1 < $1.1) })
expectedNames = sortedNames.map({ $0.0 })
fetchedNames = results.fetchedObjects!.map({ $0.valueForKey("name") as! String })
XCTAssertNotNil(results.fetchedObjects, "Expected fetched objects after re-performing fetch.")
XCTAssertEqual(results.fetchedObjects!.count, 5, "Expected a larger set of sample objects to be fetched.")
XCTAssertEqual(expectedNames, fetchedNames, "Fetched results after predicate change was incorrect.")
// Because of the change in both predicate and sort order, there is a mix of insertions, deletions, and moves.
XCTAssertEqual(changes.count, 6, "Expected events on the delegate.")
for _ in 0..<6 {
switch changes.removeFirst() {
case .Insert(object: sampleObjects["Caitlin"]!, newIndex: 2):
break
case .Insert(object: sampleObjects["Shane"]!, newIndex: 0):
break
case .Insert(object: sampleObjects["Jinger"]!, newIndex: 3):
break
case .Delete(object: sampleObjects["Tague"]!, index: 2):
break
case .Move(object: sampleObjects["Sam"]!, index: 0, newIndex: 4):
break
case .Move(object: sampleObjects["Scott"]!, index: 1, newIndex: 1):
break
default:
XCTFail("Not an expected change.")
}
}
}
}
//
// SectionedFetchedResults.swift
// DungeonMaster
//
// Created by Scott James Remnant on 3/14/16.
// Copyright © 2016 Scott James Remnant. All rights reserved.
//
import CoreData
import Foundation
public struct FetchedResultsSectionInfo<Entity: NSManagedObject> {
public private(set) var name: String
public private(set) var numberOfObjects: Int
public private(set) var objects: [Entity]
init(name: String, object: Entity) {
self.name = name
self.numberOfObjects = 1
self.objects = [object]
}
}
public class SectionedFetchedResults<Entity: NSManagedObject> {
/// Returns the name of the section for the given object.
public let sectionNameForObject: (Entity) -> String
/// The list of keys used by `sectionNameForObject`.
///
/// When this is non-`nil`, the section of an object will only be recalculated if the value of one of these keys is changed, otherwise the section is recalculated on every change.
public let sectionKeys: [String]?
/// Internal `Set` equivalent of `sectionKeys` for efficiency.
private let _sectionKeys: Set<String>?
private var fetchedResults: FetchedResults<Entity>!
public var fetchRequest: NSFetchRequest {
return fetchedResults.fetchRequest
}
public var managedObjectContext: NSManagedObjectContext {
return fetchedResults.managedObjectContext
}
public var fetchedObjects: [Entity]? {
return fetchedResults.fetchedObjects
}
public init(fetchRequest: NSFetchRequest, managedObjectContext: NSManagedObjectContext, sectionNameForObject: (Entity) -> String, sectionKeys: [String]?) {
self.sectionNameForObject = sectionNameForObject
self.sectionKeys = sectionKeys
_sectionKeys = sectionKeys.map({ Set($0) })
self.fetchedResults = FetchedResults(fetchRequest: fetchRequest, managedObjectContext: managedObjectContext, handleChanges: self.handleFetchedResultsChanges)
}
/// The sections for the fetch results.
///
/// The value is `nil` until `performFetch()` is called.
public private(set) var sections: [FetchedResultsSectionInfo<Entity>]?
public func performFetch(notifyChanges notifyChanges: Bool = false) throws {
try fetchedResults.performFetch(notifyChanges: notifyChanges)
if let objects = fetchedObjects where !notifyChanges {
sections = sortObjectsIntoSections(objects)
}
}
private func sortObjectsIntoSections(objects: [Entity]) -> [FetchedResultsSectionInfo<Entity>] {
var newSections: [FetchedResultsSectionInfo<Entity>] = []
var sectionNames: [String] = []
for object in objects {
let sectionName = sectionNameForObject(object)
if let index = sectionNames.indexOf(sectionName) {
newSections[index].numberOfObjects += 1
newSections[index].objects.append(object)
} else {
let section = FetchedResultsSectionInfo(name: sectionName, object: object)
newSections.append(section)
sectionNames.append(sectionName)
}
}
return newSections.sort({ $0.name < $1.name })
}
private func handleFetchedResultsChanges(changes: [FetchedResultsChange<Entity>]) {
for change in changes {
switch change {
case let .Insert(object: object, newIndex: newIndex):
// Object needs section calculated
break
case let .Delete(object: object, index: index):
// Lookup object in section cache.
break
case let .Move(object: object, index: index, newIndex: newIndex):
// might be the same as below or as above?
break
case let .Update(object: object, index: index):
if _sectionKeys?.isSubsetOf(object.changedValuesForCurrentEvent().keys) ?? true {
// Object should have section recalculated
}
break
}
}
}
}
//
// SectionedFetchedResultsTests.swift
// DungeonMaster
//
// Created by Scott James Remnant on 3/14/16.
// Copyright © 2016 Scott James Remnant. All rights reserved.
//
import CoreData
import XCTest
@testable import DungeonMaster
class SectionedFetchedResultsTests: XCTestCase {
let sampleData = [
( "Scott", 35, 0 ),
( "Shane", 29, 0 ),
( "Tague", 43, 0 ),
( "Jinger", 30, 1 ),
( "Sam", 34, 1 ),
( "Caitlin", 13, 1 )
]
var sampleObjects: [String:NSManagedObject] = [:]
lazy var managedObjectModel: NSManagedObjectModel = {
let model = NSManagedObjectModel()
let person = NSEntityDescription()
person.name = "Person"
let nameAttribute = NSAttributeDescription()
nameAttribute.name = "name"
nameAttribute.attributeType = .StringAttributeType
person.properties.append(nameAttribute)
let ageAttribute = NSAttributeDescription()
ageAttribute.name = "age"
ageAttribute.attributeType = .Integer16AttributeType
person.properties.append(ageAttribute)
let sexAttribute = NSAttributeDescription()
sexAttribute.name = "sex"
sexAttribute.attributeType = .Integer16AttributeType
person.properties.append(sexAttribute)
model.entities.append(person)
return model
}()
var managedObjectContext: NSManagedObjectContext!
func makePerson(name name: String, age: Int, sex: Int) -> NSManagedObject {
let personEntity = NSEntityDescription.entityForName("Person", inManagedObjectContext: managedObjectContext)!
let person = NSManagedObject(entity: personEntity, insertIntoManagedObjectContext: managedObjectContext)
person.setValue(name, forKey: "name")
person.setValue(age, forKey: "age")
person.setValue(sex, forKey: "sex")
return person
}
override func setUp() {
super.setUp()
// Set up the in-memory store.
let persistentStoreCoordinator = NSPersistentStoreCoordinator(managedObjectModel: self.managedObjectModel)
try! persistentStoreCoordinator.addPersistentStoreWithType(NSInMemoryStoreType, configuration: nil, URL: nil, options: nil)
managedObjectContext = NSManagedObjectContext(concurrencyType: .MainQueueConcurrencyType)
managedObjectContext.persistentStoreCoordinator = persistentStoreCoordinator
// Set up the sample data.
sampleObjects = [:]
for (name, age, sex) in sampleData {
let person = makePerson(name: name, age: age, sex: sex)
sampleObjects[name] = person
}
try! managedObjectContext.save()
}
override func tearDown() {
// Put teardown code here. This method is called after the invocation of each test method in the class.
super.tearDown()
}
func testExample() {
// This is an example of a functional test case.
// Use XCTAssert and related functions to verify your tests produce the correct results.
}
func testPerformanceExample() {
// This is an example of a performance test case.
self.measureBlock {
// Put the code you want to measure the time of here.
}
}
/*
func testSectionRequest() {
// Make a fetch request with a sort criteria.
let fetchRequest = NSFetchRequest(entityName: "Person")
fetchRequest.sortDescriptors = [ NSSortDescriptor(key: "age", ascending: true) ]
// Create the controller with a block that sections the objects based on their sex.
let controller = FetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: managedObjectContext, sectionForObject: {
$0.valueForKey("sex")!.stringValue
}, sectionKeys: ["sex"])
try! controller.performFetch()
XCTAssertNotNil(controller.fetchedObjects, "Expected fetched objects after performing fetch.")
XCTAssertEqual(controller.fetchedObjects!.count, 6, "Expected all sample objects to be fetched.")
var expectedNames = sampleData.sort({ $0.1 < $1.1 }).map({ $0.0 })
var fetchedNames = controller.fetchedObjects!.map({ $0.valueForKey("name") as! String })
XCTAssertEqual(expectedNames, fetchedNames, "List of fetched objects was incorrect, expected all sorted by age.")
XCTAssertNotNil(controller.sections, "Expected list of sections after performing fetch.")
XCTAssertEqual(controller.sections!.count, 2, "Expected two sections, one for each sex, to be fetched.")
var section = controller.sections![0]
XCTAssertEqual(section.name, "0", "Expected different name for first section.")
XCTAssertEqual(section.numberOfObjects, 3, "Expected three results in the first section.")
XCTAssertNotNil(section.objects, "Expected non-nil objects list for first section.")
expectedNames = sampleData.filter({ $0.2 == 0 }).sort({ $0.1 < $1.1 }).map({ $0.0 })
fetchedNames = section.objects!.map({ $0.valueForKey("name") as! String })
XCTAssertEqual(expectedNames, fetchedNames, "Contents of first section did not match that expected.")
section = controller.sections![1]
XCTAssertEqual(section.name, "1", "Expected different name for second section.")
XCTAssertEqual(section.numberOfObjects, 3, "Expected three results in the second section.")
XCTAssertNotNil(section.objects, "Expected non-nil objects list for second section.")
expectedNames = sampleData.filter({ $0.2 == 1 }).sort({ $0.1 < $1.1 }).map({ $0.0 })
fetchedNames = section.objects!.map({ $0.valueForKey("name") as! String })
XCTAssertEqual(expectedNames, fetchedNames, "Contents of second section did not match that expected.")
// FIXME check the object paths
}
func testSectionRequestWithDelegate() {
// Make a fetch request with a sort criteria.
let fetchRequest = NSFetchRequest(entityName: "Person")
fetchRequest.sortDescriptors = [ NSSortDescriptor(key: "age", ascending: true) ]
// Create the controller with a block that sections the objects based on their first initial.
let controller = FetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: managedObjectContext, sectionForObject: {
"\(($0.valueForKey("name") as! String).characters.first!)"
}, sectionKeys: ["name"])
let testDelegate = TestDelegate()
controller.delegate = testDelegate
try! controller.performFetch()
XCTAssertNotNil(controller.fetchedObjects, "Expected fetched objects after performing fetch.")
XCTAssertEqual(controller.fetchedObjects!.count, 6, "Expected all sample objects to be fetched.")
var expectedNames = sampleData.sort({ $0.1 < $1.1 }).map({ $0.0 })
var fetchedNames = controller.fetchedObjects!.map({ $0.valueForKey("name") as! String })
XCTAssertEqual(expectedNames, fetchedNames, "List of fetched objects was incorrect, expected all sorted by age.")
XCTAssertNotNil(controller.sections, "Expected list of sections after performing fetch.")
XCTAssertEqual(controller.sections!.count, 4, "Expected four sections.")
var section = controller.sections![0]
XCTAssertEqual(section.name, "C", "Expected different name for section.")
XCTAssertEqual(section.numberOfObjects, 1, "Expected one result in the section.")
XCTAssertNotNil(section.objects, "Expected non-nil objects list for section.")
fetchedNames = section.objects!.map({ $0.valueForKey("name") as! String })
XCTAssertEqual(fetchedNames, [ "Caitlin" ], "Contents of section did not match that expected.")
section = controller.sections![1]
XCTAssertEqual(section.name, "J", "Expected different name for section.")
XCTAssertEqual(section.numberOfObjects, 1, "Expected one result in the section.")
XCTAssertNotNil(section.objects, "Expected non-nil objects list for section.")
fetchedNames = section.objects!.map({ $0.valueForKey("name") as! String })
XCTAssertEqual(fetchedNames, [ "Jinger" ], "Contents of section did not match that expected.")
section = controller.sections![2]
XCTAssertEqual(section.name, "S", "Expected different name for section.")
XCTAssertEqual(section.numberOfObjects, 3, "Expected one result in the section.")
XCTAssertNotNil(section.objects, "Expected non-nil objects list for section.")
fetchedNames = section.objects!.map({ $0.valueForKey("name") as! String })
XCTAssertEqual(fetchedNames, [ "Shane", "Sam", "Scott" ], "Contents of section did not match that expected.")
section = controller.sections![3]
XCTAssertEqual(section.name, "T", "Expected different name for section.")
XCTAssertEqual(section.numberOfObjects, 1, "Expected one result in the section.")
XCTAssertNotNil(section.objects, "Expected non-nil objects list for section.")
fetchedNames = section.objects!.map({ $0.valueForKey("name") as! String })
XCTAssertEqual(fetchedNames, [ "Tague" ], "Contents of section did not match that expected.")
XCTAssertEqual(testDelegate.events.count, 0, "Expected no events on the delegate.")
// Insert an object that should end up in an existing section. The delegate should notify of us of an insert within that section, and both the fetched results, and the sections object should be updated.
makePerson(name: "Jordan", age: 31, sex: 0)
try! managedObjectContext.save()
XCTAssertNotNil(controller.fetchedObjects, "Fetched objects should not have become nil.")
XCTAssertEqual(controller.fetchedObjects!.count, 7, "Expected object count to have increased.")
expectedNames.insert("Jordan", atIndex: 3)
fetchedNames = controller.fetchedObjects!.map({ $0.valueForKey("name") as! String })
XCTAssertEqual(expectedNames, fetchedNames, "List of fetched objects didn't match that expected.")
XCTAssertNotNil(controller.sections, "List of sections should not have become nil.")
XCTAssertEqual(controller.sections!.count, 4, "Expected section count to have remained the same.")
section = controller.sections![0]
XCTAssertEqual(section.name, "C", "Expected section name to have remained the same.")
XCTAssertEqual(section.numberOfObjects, 1, "Expected section object count to have remained the same.")
XCTAssertNotNil(section.objects, "Section objects list should not have become nil.")
section = controller.sections![1]
XCTAssertEqual(section.name, "J", "Expected section name to have remained the same.")
XCTAssertEqual(section.numberOfObjects, 2, "Expected section object count to have increased.")
XCTAssertNotNil(section.objects, "Section objects list should not have become nil.")
fetchedNames = section.objects!.map({ $0.valueForKey("name") as! String })
XCTAssertEqual(fetchedNames, [ "Jinger", "Jordan" ], "Contents of section did not match that expected.")
section = controller.sections![2]
XCTAssertEqual(section.name, "S", "Expected section name to have remained the same.")
XCTAssertEqual(section.numberOfObjects, 3, "Expected section object count to have remained the same.")
XCTAssertNotNil(section.objects, "Section objects list should not have become nil.")
section = controller.sections![3]
XCTAssertEqual(section.name, "T", "Expected section name to have remained the same.")
XCTAssertEqual(section.numberOfObjects, 1, "Expected section object count to have remained the same.")
XCTAssertNotNil(section.objects, "Section objects list should not have become nil.")
XCTAssertEqual(testDelegate.events.count, 3, "Expected events on the delegate.")
switch testDelegate.events.removeFirst() {
case .willChangeContent:
break
default:
XCTFail("Expected first delegate event to be willChangeContent.")
}
switch testDelegate.events.removeFirst() {
case let .didChangeObject(object: insertedObject, indexPath: nil, type: .Insert, newIndexPath: newIndexPath?):
XCTAssertEqual((insertedObject.valueForKey("name") as! String), "Jordan", "Expected a different object to be inserted.")
XCTAssertEqual(newIndexPath.section, 1, "Index path had wrong section")
XCTAssertEqual(newIndexPath.row, 1, "Index path had wrong row.")
default:
XCTFail("Expected didChangeObject delegate event to be valid insert.")
}
switch testDelegate.events.removeFirst() {
case .didChangeContent:
break
default:
XCTFail("Expected final delegate event to be didChangeContent.")
}
// Insert an object that should end up in a new section. The delegate should notify us of the creation of the new section, followed by the insert within it. Both the fetched results, and the sections object, should be updated.
makePerson(name: "Ryan", age: 32, sex: 0)
try! managedObjectContext.save()
XCTAssertNotNil(controller.fetchedObjects, "Fetched objects should not have become nil.")
XCTAssertEqual(controller.fetchedObjects!.count, 8, "Expected object count to have increased.")
expectedNames.insert("Ryan", atIndex: 4)
fetchedNames = controller.fetchedObjects!.map({ $0.valueForKey("name") as! String })
XCTAssertEqual(expectedNames, fetchedNames, "List of fetched objects didn't match that expected.")
XCTAssertNotNil(controller.sections, "List of sections should not have become nil.")
XCTAssertEqual(controller.sections!.count, 5, "Expected section count to have increased.")
section = controller.sections![0]
XCTAssertEqual(section.name, "C", "Expected section name to have remained the same.")
XCTAssertEqual(section.numberOfObjects, 1, "Expected section object count to have remained the same.")
XCTAssertNotNil(section.objects, "Section objects list should not have become nil.")
section = controller.sections![1]
XCTAssertEqual(section.name, "J", "Expected section name to have remained the same.")
XCTAssertEqual(section.numberOfObjects, 2, "Expected section object count to have remained the same.")
XCTAssertNotNil(section.objects, "Section objects list should not have become nil.")
section = controller.sections![2]
XCTAssertEqual(section.name, "R", "Expected new section name.")
XCTAssertEqual(section.numberOfObjects, 1, "Expected section object count not correct.")
XCTAssertNotNil(section.objects, "Section objects list should not be nil.")
fetchedNames = section.objects!.map({ $0.valueForKey("name") as! String })
XCTAssertEqual(fetchedNames, [ "Ryan" ], "Contents of section did not match that expected.")
section = controller.sections![3]
XCTAssertEqual(section.name, "S", "Expected section name to have remained the same.")
XCTAssertEqual(section.numberOfObjects, 3, "Expected section object count to have remained the same.")
XCTAssertNotNil(section.objects, "Section objects list should not have become nil.")
section = controller.sections![4]
XCTAssertEqual(section.name, "T", "Expected section name to have remained the same.")
XCTAssertEqual(section.numberOfObjects, 1, "Expected section object count to have remained the same.")
XCTAssertNotNil(section.objects, "Section objects list should not have become nil.")
XCTAssertEqual(testDelegate.events.count, 4, "Expected events on the delegate.")
switch testDelegate.events.removeFirst() {
case .willChangeContent:
break
default:
XCTFail("Expected first delegate event to be willChangeContent.")
}
switch testDelegate.events.removeFirst() {
case let .didChangeSection(sectionInfo: sectionInfo, sectionIndex: sectionIndex, type: .Insert):
XCTAssertEqual(sectionInfo.name, "R", "Section has wrong name.")
XCTAssertEqual(sectionIndex, 2, "Section had wrong index.")
default:
XCTFail("Expected didChangeSection delegate event to be valid insert.")
}
switch testDelegate.events.removeFirst() {
case let .didChangeObject(object: insertedObject, indexPath: nil, type: .Insert, newIndexPath: newIndexPath?):
XCTAssertEqual((insertedObject.valueForKey("name") as! String), "Jordan", "Expected a different object to be inserted.")
XCTAssertEqual(newIndexPath.section, 1, "Index path had wrong section")
XCTAssertEqual(newIndexPath.row, 1, "Index path had wrong row.")
default:
XCTFail("Expected didChangeObject delegate event to be valid insert.")
}
switch testDelegate.events.removeFirst() {
case .didChangeContent:
break
default:
XCTFail("Expected final delegate event to be didChangeContent.")
}
}
*/
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment