Swift NSFetchedResultsController Trickery
One of my absolute favorite classes in the Cocoa Touch framework is NSFetchedResultsController
, because it makes data driven animations so easy to implement. Moving things around is how we organize in the physical world, which is why spatial position and animation of it is such an intuitive metaphor for dealing with digital objects.
In this post, I will show you how to push NSFetchedResultsController
to and beyond its limits to achieve a fluid and intuitive user interface for organizing things in a table view. These are the topics we are going to cover:
- Using a meta data entity for fetching
- Arbitrary section ordering
- Animating between different grouping of data
- Semantic drag 'n' drop
- Showing empty sections through a proxy
The demo app
To demonstrate the powers of the fetched results controller, I have built an app. This simple task manager lets you enter todos with three different priorities and also has an option for showing a simpler list where priority does not matter.
To switch priority of the tasks, we could have used a detail inspector view, action sheet or cell swiping. Those are examples of indirect manipulation, making the user need to realize that changing an attribute of the task, results in the task being reorganized in the list. Rather than that, let us make the user just drag 'n' drop the task into the section where it would end up showing after manipulating the corresponding attribute. Direct manipulation gives instant feedback and a more natural interaction.
The full source code for the demo app can be found at GitHub.
The data model
If we look at the data model file for this app, we see that the ToDo
entity is very simple, containing what you would expect: a title
, priority
and a done
flag. What is more interesting is that each todo has a mandatory relation with a meta data object, which is created upon insert.
override func awakeFromInsert() {
super.awakeFromInsert()
metaData = NSEntityDescription.insertNewObjectForEntityForName(ToDoMetaData.entityName, inManagedObjectContext: managedObjectContext) as ToDoMetaData
}
Extracting meta data into a separate entity separates concerns and makes it easier if we would want to serialize todos to a web service in the future. In the app, the NSFetchedResultsController
will actually be set up to fetch the meta data objects instead of the todos themselves. This will soon be explained why.
The first property of the meta data entity, internalOrder
, is an integer which orders the todos within a section. It gets updated when the user reorders the list with drag 'n' drop.
The second property, sectionIdentifier
, is a string used to divide the todos into sections. NSFetchedResultsController
requires that the sections are sorted in the same order as the rows so they can be fetched in batches. Since the sorting attributes must be stored in the persistent store, that also means the sectionNameKeyPath
must either be stored itself or derived from a stored property.
Every time a property affecting a todo's displayed section changes (such as priority
or done
status), the section identifier must be updated. In Objective-C we could accomplish this with property overrides or KVO but unfortunately neither of these techniques are easily available for managed objects in Swift (as of Xcode 6 beta 5). Instead we have to remember to manually trigger an update when doing drag 'n' drop or marking a todo as done.
public func updateSectionIdentifier() {
sectionIdentifier = sectionForCurrentState().toRaw()
}
private func sectionForCurrentState() -> ToDoSection {
if toDo.done.boolValue {
return .Done
} else if listConfiguration.listMode == ToDoListMode.Simple {
return .ToDo
} else {
switch ToDoPriority.fromRaw(toDo.priority)! {
case .Low: return .LowPriority
case .Medium: return .MediumPriority
case .High: return .HighPriority
}
}
}
Another nuisance is that NSFetchedResultsController
assumes all sections are semantically equal and therefor always sorts them in alphabetical order. We get around this by assigning the
sectionNameKeyPath
to a Swift enum
value with a method for getting the actual display name. This way, we achieve arbitrary sorting of sections. If we wanted to make the order of the sections dynamic, we could instead of using an enum, generate an identifier string runtime.
enum ToDoSection: String {
case ToDo = "10"
case HighPriority = "11"
case MediumPriority = "12"
case LowPriority = "13"
case Done = "20"
func title() -> String {
switch self {
case ToDo: return "Left to do"
case Done: return "Done"
case HighPriority: return "High priority"
case MediumPriority: return "Medium priority"
case LowPriority: return "Low priority"
}
}
}
At last we have the ToDoListConfiguration
. This object stores in which display mode (simple/prioritized) the table view is in. This could as well have been stored in NSUserDefaults
or just in memory using state restoration between app launches. But since it affects the section identifier of the meta data objects, I found it suitable to have it connected in the persistent store.
Setting up the NSFetchedResultsController
private lazy var toDosController: NSFetchedResultsController = {
let fetchRequest = NSFetchRequest(entityName: ToDoMetaData.entityName)
fetchRequest.relationshipKeyPathsForPrefetching = ["toDo"]
fetchRequest.sortDescriptors = [NSSortDescriptor(key: "sectionIdentifier", ascending: true), NSSortDescriptor(key: "internalOrder", ascending: false)]
let controller = NSFetchedResultsController(fetchRequest: fetchRequest,
managedObjectContext: self.managedObjectContext,
sectionNameKeyPath: "sectionIdentifier",
cacheName: nil)
controller.performFetch(nil)
controller.delegate = self
return controller
}()
As we setup the fetched results controller, there are a couple of things to note. First, the entity we fetch is ToDoMetaData
and not ToDo
itself. The reason is that this is where our sorting keys and sectionNameKeyPath
are stored. The fetched results controller can be setup with key paths directing to a related entity, but it only keeps track of changes on its original fetched dataset.
Since the meta data object has a one to one relationship to the real ToDo
object, we can easily extract the todo once we have the metadata. To make this perform better, we add "toDo" to the relationshipKeyPathsForPrefetching
property of the fetch request. Else, CoreData would fire a fault to the persistent store for every single todo once they are displayed in the table view.
The fetched results controller delegate calls which notify of changes to sections and objects are forwarded to a separate FetchControllerDelegate
class, which uses an implementation mostly like the example found in the developer library. It responds to the delegate calls by informing the table view.
Animating change of done-state and display mode
With the data model and fetched results controller set up, it is dead simple to produce data driven animations.
override func tableView(tableView: UITableView!, didSelectRowAtIndexPath indexPath: NSIndexPath!) {
tableView.deselectRowAtIndexPath(indexPath, animated: true)
let toDo = toDoListController.toDoAtIndexPath(indexPath)
toDo.done = !toDo.done.boolValue
toDo.metaData.updateSectionIdentifier()
toDo.managedObjectContext.save(nil)
}
As we change done status, the meta data object will assign a new section identifier which the fetched results controller picks up to notify that the todo has moved from one section to another.
Switching display modes between simple and prioritized is even more trivial. This is where NSFetchedResultsController
shines through, making the view controller light and not having to calculate which index paths should move where.
// ToDoViewController
@IBAction func viewModeControlChanged(sender: UISegmentedControl) {
let configuration = ToDoListConfiguration.defaultConfiguration(managedObjectContext)
let selectedIndex = sender.selectedSegmentIndex
configuration.listMode = selectedIndex == 0 ? .Simple : .Prioritized
}
// ToDoListConfiguration
var listMode: ToDoListMode {
set {
listModeValue = newValue.toRaw()
for metaData in toDoMetaData.allObjects as [ToDoMetaData] {
metaData.updateSectionIdentifier()
}
}
}
Semantic drag 'n' drop
In this app, reorder controls serve two purposes: to visually reorder todos and to change their state. You can move a task to the "Done" section to mark it as finished or between different priority sections. Both effects are pretty straight forward to achieve. The only thing to remember is to ignore the fetched results controller who will want to notify of the changes to the model.
override func tableView(tableView: UITableView!, moveRowAtIndexPath sourceIndexPath: NSIndexPath!, toIndexPath destinationIndexPath: NSIndexPath!) {
if sourceIndexPath == destinationIndexPath {
return
}
// Don't let fetched results controller affect table view
fetchControllerDelegate.ignoreNextUpdates = true
let toDo = toDoListController.toDoAtIndexPath(sourceIndexPath)
// Update todo with new done & priority state
...
toDo.managedObjectContext.save(nil)
}
Showing empty sections through a proxy
One of the bigger problems with NSFetchedResultsController
is that it cannot show empty sections. This is because it derives sections from objects and not the other way around. Since we use reorder controls to change priorities for todos, we sometimes need to show empty sections for the user to drag todos there. The solution is to wrap the fetched results controller in a proxy object.
In this project, the ToDoListController
class acts as a man-in-the-middle by forwarding delegate calls and section information from its fetched results controller and modifying them along the way. When the table view enters edit mode, the list controller generates the empty sections and tells its delegate that they have been inserted.
public var sections: [ControllerSectionInfo] = []
var showsEmptySections: Bool = false {
didSet {
if showsEmptySections == oldValue { return }
delegate?.controllerWillChangeContent?(toDosController)
let changedEmptySections = sectionInfoWithEmptySections(true)
notifyDelegateOfChangedEmptySections(changedEmptySections,
changeType: showsEmptySections ? .Insert : .Delete)
sections = sectionInfoWithEmptySections(showsEmptySections)
delegate?.controllerDidChangeContent?(toDosController)
}
}
Since the fetched results controller is not aware of these "fake" sections, all index paths it sends must be converted to match the expanded table structure. ToDoListController
does this by keeping track of which sections are known to the fetched results controller and what indexes they have there.
public class ControllerSectionInfo: NSFetchedResultsSectionInfo {
let section: ToDoSection
private let fetchedIndex: Int?
// Protocol implementation...
}
// ToDoListController
public func controller(controller: NSFetchedResultsController!, didChangeObject anObject: AnyObject!, atIndexPath indexPath: NSIndexPath!, forChangeType type: NSFetchedResultsChangeType, newIndexPath: NSIndexPath!) {
let displayedOldIndexPath = displayedIndexPathForFetchedIndexPath(indexPath, sections: oldSectionsDuringFetchUpdate)
let displayedNewIndexPath = displayedIndexPathForFetchedIndexPath(newIndexPath, sections: sections)
let metaData = anObject as ToDoMetaData
delegate?.controller?(controller, didChangeObject: metaData.toDo, atIndexPath: displayedOldIndexPath, forChangeType: type, newIndexPath: displayedNewIndexPath)
}
Conclusion
Hope you enjoyed this sample of techniques on taming the NSFetchedResultsController
. You may have noticed that the app disables the display mode control in edit mode. I have left this as a final exercise, since I felt the necessary changes would clutter the code.
If you have feedback on how these techniques can be implemented even better (especially in these early times of Swift), hit me up on Twitter or send a pull request on the repo for this demo app. Thanks for reading!