Created
September 26, 2015 20:58
-
-
Save Adlai-Holler/a67f7a96779952ed5062 to your computer and use it in GitHub Desktop.
An operation to update an ASTableView/ASCollectionView from an NSFetchedResultsController changeset
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
Abstract: An operation the apply an FRC update to a table view/collection view. | |
*/ | |
import UIKit | |
struct CollectionUpdate { | |
// Note: These properties are listed in the order changes should be processed | |
// Indexes are pre-update | |
var updatedItems: Set<NSIndexPath> | |
// Indexes are pre-update | |
var deletedSections: NSIndexSet | |
// Indexes are pre-update | |
var deletedItems: Set<NSIndexPath> | |
// Indexes are post-update | |
var insertedSections: NSIndexSet | |
// Indexes are post-update | |
var insertedItems: Set<NSIndexPath> | |
} | |
final class UpdateCollectionViewOperation<Element: NSManagedObject, Node: ASCellNode>: GroupOperation { | |
var animated: Bool = true | |
weak var tableView: ASTableView? | |
weak var collectionView: ASCollectionView? | |
/// This will be called on the main thread (inside a performBlockAndWait if you provide a context) | |
let attemptUpdateNode: ((Node, Element) -> Bool)? | |
/// Note: the provided index path may be the index path after the update (insert), or it may be before (update) | |
let createNode: ((NSIndexPath, Element) -> Node) | |
/// Provide the pre-update data from the data source. This will be called once on an arbitrary queue near the beginning of the op | |
let getInitialData: (() -> [[Node]]) | |
/// Set the new data for the data source. This will be called on the main queue during the view update | |
let applyFinalData: ([[Node]] -> Void) | |
let change: FRCChangeDetails | |
let managedObjectContext: NSManagedObjectContext | |
init(change: FRCChangeDetails, context: NSManagedObjectContext, getInitialData: () -> [[Node]], applyFinalData: [[Node]] -> Void, createNode: ((NSIndexPath, Element) -> Node), attemptUpdateNode: ((Node, Element) -> Bool)? = nil) { | |
self.applyFinalData = applyFinalData | |
self.getInitialData = getInitialData | |
self.change = change | |
self.managedObjectContext = context | |
self.createNode = createNode | |
self.attemptUpdateNode = attemptUpdateNode | |
super.init(operations: []) | |
} | |
override func execute() { | |
var suboperations: [Operation] = [] | |
let update: CollectionUpdate = change.coalescedChanges | |
var newData = getInitialData() | |
let reportedDeletes = update.deletedItems | |
.filter { !update.deletedSections.containsIndex($0.section) } | |
.sort { $0.compare($1) == .OrderedDescending } | |
let reportedInserts = update.insertedItems | |
.filter { !update.insertedSections.containsIndex($0.section) } | |
.sort { $0.compare($1) == .OrderedAscending } | |
// Attempt to update visible items immediately on the main queue, to avoid flashing or whatever | |
var unhandledUpdatedItems = update.updatedItems | |
let updateVisibleNodesOp: Operation | |
if let attemptUpdateNode = attemptUpdateNode where !unhandledUpdatedItems.isEmpty { | |
updateVisibleNodesOp = BlockOperation(mainQueueBlock: { [weak self] in | |
guard let strongSelf = self else { return } | |
let tableView = strongSelf.tableView | |
let collectionView = strongSelf.collectionView | |
guard strongSelf.tableView != nil || strongSelf.collectionView != nil else { return } | |
// update visible rows whose nodes are updated | |
let visibleIndexPaths = tableView?.indexPathsForVisibleRows ?? collectionView?.indexPathsForVisibleItems() ?? [] | |
let visibleUpdated = unhandledUpdatedItems.intersect(visibleIndexPaths) | |
if visibleUpdated.isEmpty { return } | |
strongSelf.managedObjectContext.performBlockAndWait { | |
assert(NSThread.isMainThread()) | |
for indexPath in visibleUpdated { | |
let node = (tableView?.nodeForRowAtIndexPath(indexPath) ?? collectionView!.nodeForItemAtIndexPath(indexPath)) as! Node | |
let element = strongSelf.change.dataBeforeUpdate.objectAtIndexPath(indexPath) as! Element | |
if attemptUpdateNode(node, element) { | |
unhandledUpdatedItems.remove(indexPath) | |
} | |
} | |
} | |
}) | |
} else { | |
updateVisibleNodesOp = BlockOperation(block: nil) | |
} | |
suboperations.append(updateVisibleNodesOp) | |
updateVisibleNodesOp.name = "Update visible nodes" | |
let computeNewDataOp = BlockOperation(context: managedObjectContext) { [weak self] in | |
guard let strongSelf = self else { return } | |
// 1. update items (uses old indexes, does not change indexes) | |
// We attempt to update our existing nodes, but in some cases we need to create new nodes | |
// NOTE: we assume that newData has not been modified before this point | |
for indexPath in unhandledUpdatedItems { | |
let element = strongSelf.change.dataBeforeUpdate.objectAtIndexPath(indexPath) as! Element | |
let node = newData[indexPath.section][indexPath.item] | |
if let attemptUpdateNode = strongSelf.attemptUpdateNode where !node.nodeLoaded && attemptUpdateNode(node, element) { | |
unhandledUpdatedItems.remove(indexPath) | |
} else { | |
let newNode = strongSelf.createNode(indexPath, element) | |
newData[indexPath.section][indexPath.item] = newNode | |
} | |
} | |
// 2. deleted items descending (need section indexes to be preserved) | |
for indexPath in reportedDeletes { | |
newData[indexPath.section].removeAtIndex(indexPath.item) | |
} | |
// 3. deleted sections descending (last bit that uses the old indices) | |
update.deletedSections.enumerateRangesWithOptions(.Reverse) { nsRange, stop in | |
let range = nsRange.range | |
// newHeaders.removeRange(range) | |
newData.removeRange(range) | |
} | |
// 4. inserted sections ascending (we need to get section indexes right before item inserts) | |
update.insertedSections.enumerateRangesUsingBlock { nsRange, stop in | |
let range = nsRange.range | |
// let insertedHeaders = self.change.dataAfterUpdate[range].map { section in TripHeaderNode(year: section.name) } | |
// newHeaders.insertContentsOf(insertedHeaders, at: range.startIndex) | |
let sections: [[Node]] = range.map { section in | |
let elements = strongSelf.change.dataAfterUpdate[section].objects as! [Element] | |
return elements.enumerate().map { i, element in | |
let indexPath = NSIndexPath(forItem: i, inSection: section) | |
return strongSelf.createNode(indexPath, element) | |
} | |
} | |
newData.insertContentsOf(sections, at: range.startIndex) | |
} | |
// 5. inserted items ascending | |
for indexPath in reportedInserts { | |
let element = strongSelf.change.dataAfterUpdate.objectAtIndexPath(indexPath) as! Element | |
let newNode = strongSelf.createNode(indexPath, element) | |
newData[indexPath.section].insert(newNode, atIndex: indexPath.row) | |
} | |
} | |
suboperations.append(computeNewDataOp) | |
computeNewDataOp.name = "Compute new data" | |
let submitUpdateOp = BlockOperation(block: { [weak self] continuation in | |
guard let strongSelf = self else { return } | |
dispatch_async(dispatch_get_main_queue()) { | |
if let tableView = strongSelf.tableView { | |
tableView.beginUpdates() | |
strongSelf.applyFinalData(newData) | |
if !unhandledUpdatedItems.isEmpty { | |
tableView.reloadRowsAtIndexPaths(Array(unhandledUpdatedItems), withRowAnimation: .None) | |
} | |
if !reportedDeletes.isEmpty { | |
tableView.deleteRowsAtIndexPaths(reportedDeletes, withRowAnimation: .Automatic) | |
} | |
if update.deletedSections.count > 0 { | |
tableView.deleteSections(update.deletedSections, withRowAnimation: .Automatic) | |
} | |
if update.insertedSections.count > 0 { | |
tableView.insertSections(update.insertedSections, withRowAnimation: .Automatic) | |
} | |
if reportedInserts.count > 0 { | |
tableView.insertRowsAtIndexPaths(reportedInserts, withRowAnimation: .Automatic) | |
} | |
tableView.endUpdatesAnimated(strongSelf.animated, completion: { finished in | |
continuation() | |
}) | |
} else if let collectionView = self?.collectionView { | |
collectionView.performBatchAnimated(strongSelf.animated, updates: { | |
strongSelf.applyFinalData(newData) | |
if !unhandledUpdatedItems.isEmpty { | |
collectionView.reloadItemsAtIndexPaths(Array(unhandledUpdatedItems)) | |
} | |
if !reportedDeletes.isEmpty { | |
collectionView.deleteItemsAtIndexPaths(reportedDeletes) | |
} | |
if update.deletedSections.count > 0 { | |
collectionView.deleteSections(update.deletedSections) | |
} | |
if update.insertedSections.count > 0 { | |
collectionView.insertSections(update.deletedSections) | |
} | |
if reportedInserts.count > 0 { | |
collectionView.insertItemsAtIndexPaths(reportedInserts) | |
} | |
}, completion: { finished in | |
continuation() | |
}) | |
} | |
} | |
}) | |
suboperations.append(submitUpdateOp) | |
submitUpdateOp.name = "Submit update" | |
for (operation, nextOperation) in zip(suboperations, suboperations[1..<suboperations.count]) { | |
nextOperation.addDependency(operation) | |
} | |
for operation in suboperations { | |
addOperation(operation) | |
} | |
super.execute() | |
} | |
} |
So I modified this a bit (still in progress) because I'm using a different way of handling changesets but the concept is the same:
https://gist.github.com/smyrgl/e4922230d25d90ef8a98
Great stuff!
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
I dig that you are using Apple's Operations "pseudo-framework" for this...I've ended up using it a ton as well. They probably should just have just added it to core frameworks...it's incredibly useful.