Skip to content

Instantly share code, notes, and snippets.

@Adlai-Holler
Created September 26, 2015 20:58
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Adlai-Holler/a67f7a96779952ed5062 to your computer and use it in GitHub Desktop.
Save Adlai-Holler/a67f7a96779952ed5062 to your computer and use it in GitHub Desktop.
An operation to update an ASTableView/ASCollectionView from an NSFetchedResultsController changeset
/**
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()
}
}
@smyrgl
Copy link

smyrgl commented Sep 27, 2015

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.

@smyrgl
Copy link

smyrgl commented Sep 27, 2015

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