I have objects with NSDate property and I need to split them into two sections (first - future events, second - historic events) and then for the first section I need to sort them by the date property in ascending order and the second section in descending order. Any idea how to do the sorting?
Assuming you are using a NSFetchedResultsController
, the underlying fetch must be sorted one way or the other. I can think of two different solutions:
Use two separate FRCs, with complementary predicates so that one handles the past events, while the other handles future events. One will be sorted ascending, the other descending. The problem is that both FRCs will generate indexPaths
for section 0. You will therefore need to remap the second FRC's indexPaths
to use section 1 of the tableView. For example, in cellForRowAtIndexPath
you would need something like this:
if (indexPath.section == 0) {
objectToDisplay = self.futureFetchedResultsController.objectAtIndexPath(indexPath)
} else { // use second FRC
let frcIndexPath = NSIndexPath(forRow: indexPath.row, inSection: 0)
objectToDisplay = self.pastFetchedResultsController.objectAtIndexPath(frcIndexPath)
}
Alternatively, stick with a single FRC, sorted ascending. Then remap the indexPath
for the second section, so that the last object in the section is displayed in row 0, etc:
if (indexPath.section == 0) {
objectToDisplay = self.fetchedResultsController.objectAtIndexPath(indexPath)
} else { // use remap to reverse sort order FRC
let sectionInfo = self.fetchedResultsController.sections[1] as! NSFetchedResultsSectionInfo
let sectionCount = sectionInfo.numberOfObjects
let frcIndexPath = NSIndexPath(forRow: (sectionCount - 1 - indexPath.row), inSection:indexPath.section)
objectToDisplay = self.fetchedResultsController.objectAtIndexPath(frcIndexPath)
}
Personally I think the second option is the preferable. In each case, all the tableView datasource/delegate methods will need the same remapping, and the FRC delegate methods will need the reverse mapping.
To tackle the general problem -- desiring different ordering per section -- you can wrap multiple NSFetchedResultsController
objects into a single object which flattens the section arrays into a single array, and remaps the results of functions like func object(at: IndexPath)
as well as the index paths in the NSFetchedResultsControllerDelegate
notifications.
This will allow you to solve the general problem of wanting to display any number of sections ordered in different ways.
I had a go at creating this wrapper object (a CompoundFetchedResultsController), and it seems to work well:
/**
A CompoundFetchedResultsController is a wrapper of a number of inner NSFetchedResultsControllers.
The wrapper flattens the sections produced by the inner controllers, behaving as if all sections
were fetched by a single controller. Additionally, change notifications are mapped before being
passed to the optional NSFetchedResultsControllerDelegate property, so that the section indices
in the notifications reflect the flattened section indicies.
Example use case: a table where sections should be ordered in mutually opposing ways. E.g., if
section 1 should be ordered by propery A ascending, but section 2 should be ordered by property A
descending. In this case, two controllers can be created - one ordering ascending, the other de-
scending - and wrapped in a CompoundFetchedResultsController. This will maintain the ease of use
in a UITableViewController, and the functionality provided by a NSFetchedResultsControllerDelegate.
*/
class CompoundFetchedResultsController<T: NSFetchRequestResult>: NSObject, NSFetchedResultsControllerDelegate {
// The wrapperd controllers
let controllers: [NSFetchedResultsController<T>]
// A delegate to notify of changes. Each of the controllers' delegates are set to this class,
// so that we can map the index paths in the notifications before forwarding to this delegate.
var delegate: NSFetchedResultsControllerDelegate? {
didSet { controllers.forEach{$0.delegate = self} }
}
init(controllers: [NSFetchedResultsController<T>]) { self.controllers = controllers }
func performFetch() throws { controllers.forEach{try? $0.performFetch()} }
var sections: [NSFetchedResultsSectionInfo]? {
// To get the flattened sections array, we simply reduce-by-concatenation the inner controllers' sections arrays.
get { return controllers.flatMap{$0.sections}.reduce([], +) }
}
private func sectionOffset(forController controller: NSFetchedResultsController<T>) -> Int {
// Determine the index of the specified controller
let controllerIndex = controllers.index(of: controller)!
// Count the number of sections present in all controllers up to (but not including) the supplied controller
return controllers.prefix(upTo: controllerIndex).map{$0.sections!.count}.reduce(0, +)
}
func object(at indexPath: IndexPath) -> T {
// Sum the section counts of the controllers, in order, until we exceed the section of the supplied index path.
// At that point, we have identifiers the controller which should be used to obtain the object, and just
// adjust the supplied index path's section accordingly.
var sectionCount = 0
for controller in controllers {
if sectionCount + controller.sections!.count <= indexPath.section {
sectionCount += controller.sections!.count
}
else {
return controller.object(at: IndexPath(row: indexPath.row, section: indexPath.section - sectionCount))
}
}
fatalError("Could not find index path \(indexPath).")
}
func indexPath(forObject object: T) -> IndexPath? {
// Given an object, to determine which controller it is in, we just query each controller in turn.
for controller in controllers {
if let indexPath = controller.indexPath(forObject: object) {
return IndexPath(row: indexPath.row, section: sectionOffset(forController: controller) + indexPath.section)
}
}
return nil
}
func controllerWillChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
// Forward on the willChange notification
delegate?.controllerWillChangeContent?(controller)
}
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
// Forward on the didlChange notification
delegate?.controllerDidChangeContent?(controller)
}
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
let sectionOffset = self.sectionOffset(forController: controller as! NSFetchedResultsController<T>)
// Index Paths should be adjusted by adding to the section offset to the section index
func adjustIndexPath(_ indexPath: IndexPath?) -> IndexPath? {
guard let indexPath = indexPath else { return nil }
return IndexPath(row: indexPath.row, section: indexPath.section + sectionOffset)
}
// Forward on the notification with the adjusted index paths
delegate?.controller?(controller, didChange: anObject, at: adjustIndexPath(indexPath), for: type, newIndexPath: adjustIndexPath(newIndexPath))
}
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange sectionInfo: NSFetchedResultsSectionInfo, atSectionIndex sectionIndex: Int, for type: NSFetchedResultsChangeType) {
let sectionOffset = self.sectionOffset(forController: controller as! NSFetchedResultsController<T>)
// Forward on the notification with the adjusted section index
delegate?.controller?(controller, didChange: sectionInfo, atSectionIndex: sectionIndex + sectionOffset, for: type)
}
}
Update (October 2021) Whilst the above solution was handy at the time, if you're targetting iOS 14 or up (or equivalent), you will probably be better placed to use a simpler solution with diffable data sources and section snapshots.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With