Bartłomiej Semańczyk Bartłomiej Semańczyk - 1 year ago 65
Swift Question

NSFetchedResultsController - reversed NSIndexPath with multiply sections and pagination

To simplify my example, I develop

ChatViewController
where I need to display messages grouped into sections (one section for one day). I can load more messages swiping
UITableView
down. I need a pagination here. Messages are displayed from the oldest (top) to the latest (bottom of the table). The obvious is that I need to fetch messages sorted by
createdAt
property DESCENDING.

So this is how I setup my
NSFetcheResultsController
on
viewDidLoad()
:

private func setupFetchedResultsController() {

let context = NSManagedObjectContext.MR_defaultContext()
let fetchedRequest = NSFetchRequest(entityName: "Message")
let createdAtDescriptor = NSSortDescriptor(key: "createdAt", ascending: false)

fetchedRequest.predicate = NSPredicate(format: "conversation.identifier = %lld", identifier)
fetchedRequest.sortDescriptors = [createdAtDescriptor]

fetchedResultsController = NSFetchedResultsController(fetchRequest: fetchedRequest, managedObjectContext: context, sectionNameKeyPath: "normalizedCreatedAt", cacheName: nil)
fetchedResultsController.delegate = self

try! fetchedResultsController.performFetch()
tableView.reloadData()

debugFRC()
}


Assuming... this is pretty simple example what I fetch from
NSFetchedResultsController
:

Section 0
0 - 0 - message A, createdAt: '21-07-2016 08:42'
0 - 1 - message B, createdAt: '21-07-2016 08:40'

Section 1
1 - 0 - message C, createdAt: '20-07-2016 08:42'
1 - 1 - message D, createdAt: '20-07-2016 08:40'
1 - 2 - message E, createdAt: '20-07-2016 08:38'
1 - 3 - message F, createdAt: '20-07-2016 08:36'
1 - 4 - message G, createdAt: '20-07-2016 08:34'
1 - 5 - message H, createdAt: '20-07-2016 08:32'

Section 2
2 - 0 - message I, createdAt: '19-07-2016 08:42'
2 - 1 - message J, createdAt: '19-07-2016 08:40'
2 - 2 - message K, createdAt: '19-07-2016 08:38'


But I need to display it like this in my
UITableView
:

Section 0
0 - 0 - message K, createdAt: '19-07-2016 08:38'
0 - 1 - message J, createdAt: '19-07-2016 08:40'
0 - 2 - message I, createdAt: '19-07-2016 08:42'

Section 1
1 - 0 - message H, createdAt: '20-07-2016 08:32'
1 - 1 - message G, createdAt: '20-07-2016 08:34'
1 - 2 - message F, createdAt: '20-07-2016 08:36'
1 - 3 - message E, createdAt: '20-07-2016 08:38'
1 - 4 - message D, createdAt: '20-07-2016 08:40'
1 - 5 - message C, createdAt: '20-07-2016 08:42'

Section 2
0 - 0 - message B, createdAt: '21-07-2016 08:40'
0 - 1 - message A, createdAt: '21-07-2016 08:42'


So, I have created three helper private functions:

//MARK: - Private

private func debugFRC() {

if let sections = fetchedResultsController.sections {

for (index, section) in sections.enumerate() {

print("FRC: \(index) ::: \(section.objects!.count)")

for (index, message) in (section.objects as! [Message]).enumerate() {
print("FRC MESSAGE: \(index) --->>> \(message.createdAt)")
}
}
}
}

private func reversedIndexPathForIndexPath(indexPath: NSIndexPath) -> NSIndexPath {

let section = reversedSectionForSection(indexPath.section)
let row = fetchedResultsController.sections![section].objects!.count - indexPath.row - 1

return NSIndexPath(forRow: row, inSection: section)
}

private func reversedSectionForSection(section: Int) -> Int {
return fetchedResultsController!.sections!.count - section - 1
}


And then I have implemented my
NSFetchedResultsControllerDelegate
:

//MARK: - NSFetchedResultsControllerDelegate

func controllerWillChangeContent(controller: NSFetchedResultsController) {
tableView.beginUpdates()
}

func controller(controller: NSFetchedResultsController, didChangeSection sectionInfo: NSFetchedResultsSectionInfo, atIndex sectionIndex: Int, forChangeType type: NSFetchedResultsChangeType) {

let indexSet = NSIndexSet(index: reversedSectionForSection(sectionIndex))

switch type {
case .Insert:

tableView.insertSections(indexSet, withRowAnimation: .Fade)

case .Delete:

tableView.deleteSections(indexSet, withRowAnimation: .Fade)

case .Update:

fallthrough

case .Move:

tableView.reloadSections(indexSet, withRowAnimation: .Fade)
}
}

func controller(controller: NSFetchedResultsController, didChangeObject anObject: AnyObject, atIndexPath indexPath: NSIndexPath?, forChangeType type: NSFetchedResultsChangeType, newIndexPath: NSIndexPath?) {

switch type {
case .Insert:

if let newIndexPath = newIndexPath {
tableView.insertRowsAtIndexPaths([reversedIndexPathForIndexPath(newIndexPath)], withRowAnimation: .Fade)
}

case .Delete:

if let indexPath = indexPath {
tableView.deleteRowsAtIndexPaths([reversedIndexPathForIndexPath(indexPath)], withRowAnimation: .Fade)
}

case .Update:

if let indexPath = indexPath {
tableView.reloadRowsAtIndexPaths([reversedIndexPathForIndexPath(indexPath)], withRowAnimation: .Fade)
}

case .Move:

if let indexPath = indexPath, let newIndexPath = newIndexPath {

tableView.deleteRowsAtIndexPaths([reversedIndexPathForIndexPath(indexPath)], withRowAnimation: .Fade)
tableView.insertRowsAtIndexPaths([reversedIndexPathForIndexPath(newIndexPath)], withRowAnimation: .Fade)
}
}
}

func controllerDidChangeContent(controller: NSFetchedResultsController) {
tableView.endUpdates()
}


and
UITableViewDataSource
delegate:

//MARK: - UITableViewDataSource

func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return fetchedResultsController!.sections!.count
}

func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return fetchedResultsController.sections![reversedSectionForSection(section)].objects!.count
}

func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {

let message = fetchedResultsController?.objectAtIndexPath(reversedIndexPathForIndexPath(indexPath)) as! Message
let cell = tableView.dequeueReusableCellWithIdentifier("cellIdentifier", forIndexPath: reversedIndexPathForIndexPath(indexPath)) as! TableViewCell

cell.cellTitleLabel?.text = message.content

return cell
}


Output for
debugFRC()
is following:

FRC: 0 ::: 35
FRC MESSAGE: 0 --->>> 2016-08-23 13:02:32 +0000
FRC MESSAGE: 1 --->>> 2016-08-23 12:59:25 +0000
FRC MESSAGE: 2 --->>> 2016-08-23 11:49:07 +0000
FRC MESSAGE: 3 --->>> 2016-08-23 11:48:21 +0000
FRC MESSAGE: 4 --->>> 2016-08-23 11:32:44 +0000
FRC MESSAGE: 5 --->>> 2016-08-23 11:30:19 +0000
FRC MESSAGE: 6 --->>> 2016-08-23 11:30:16 +0000
FRC MESSAGE: 7 --->>> 2016-08-23 11:27:48 +0000
FRC MESSAGE: 8 --->>> 2016-08-23 11:23:58 +0000
FRC MESSAGE: 9 --->>> 2016-08-23 11:23:32 +0000
FRC MESSAGE: 10 --->>> 2016-08-23 11:23:21 +0000
FRC MESSAGE: 11 --->>> 2016-08-23 11:18:51 +0000
FRC MESSAGE: 12 --->>> 2016-08-23 11:18:45 +0000
FRC MESSAGE: 13 --->>> 2016-08-23 11:18:36 +0000
FRC MESSAGE: 14 --->>> 2016-08-23 11:16:23 +0000
FRC MESSAGE: 15 --->>> 2016-08-23 11:16:09 +0000
FRC MESSAGE: 16 --->>> 2016-08-23 11:15:49 +0000
FRC MESSAGE: 17 --->>> 2016-08-23 11:15:38 +0000
FRC MESSAGE: 18 --->>> 2016-08-23 11:15:29 +0000
FRC MESSAGE: 19 --->>> 2016-08-23 10:57:44 +0000
FRC MESSAGE: 20 --->>> 2016-08-23 10:57:44 +0000
FRC MESSAGE: 21 --->>> 2016-08-23 10:57:43 +0000
FRC MESSAGE: 22 --->>> 2016-08-23 10:57:43 +0000
FRC MESSAGE: 23 --->>> 2016-08-23 10:57:42 +0000
FRC MESSAGE: 24 --->>> 2016-08-23 10:57:42 +0000
FRC MESSAGE: 25 --->>> 2016-08-23 10:57:40 +0000
FRC MESSAGE: 26 --->>> 2016-08-23 10:56:31 +0000
FRC MESSAGE: 27 --->>> 2016-08-23 10:55:49 +0000
FRC MESSAGE: 28 --->>> 2016-08-23 10:55:08 +0000
FRC MESSAGE: 29 --->>> 2016-08-23 10:53:45 +0000
FRC MESSAGE: 30 --->>> 2016-08-23 10:48:59 +0000
FRC MESSAGE: 31 --->>> 2016-08-23 10:48:40 +0000
FRC MESSAGE: 32 --->>> 2016-08-23 10:47:02 +0000
FRC MESSAGE: 33 --->>> 2016-08-23 10:45:06 +0000
FRC MESSAGE: 34 --->>> 2016-08-23 10:45:01 +0000
FRC: 1 ::: 2
FRC MESSAGE: 0 --->>> 2016-08-16 09:16:38 +0000
FRC MESSAGE: 1 --->>> 2016-08-16 09:16:22 +0000


But it doesnt work. The problem is the following:


CoreData: error: Serious application error. An exception was caught from the delegate of NSFetchedResultsController during a call to -controllerDidChangeContent:. attempt to delete row 34 from section 0 which only contains 2 rows before the update with userInfo (null)


I am pretty sure I missed sth, but I do not know what. Any ideas?


I will start and award a bounty of 200 for the one who help me with this

Answer Source

Can you check this answer?
It does the same thing you want to do.

The key is creating ** transient field** as Check mark the transient field in your data model for particular attribute(e.g. sectionTitle).

And calculating it as:

Message{
@NSManaged var body: String?
@NSManaged var seen: NSNumber?
@NSManaged var time: NSDate?
@NSManaged var uid: String?
@NSManaged var conversation: Conversation?

var sectionTitle: String? {
    //this is **transient** property
    //to set it as transient, check mark the box with same name in data model
    return time!.getTimeStrWithDayPrecision()
}
}

Then initialise NSFetchedResultsController as :

let request = NSFetchRequest(entityName: "Message")
let sortDiscriptor = NSSortDescriptor(key: "time", ascending: true)
request.sortDescriptors = [sortDiscriptor]

let pred = NSPredicate(format: "conversation.contact.uid == %@", _conversation!.contact!.uid!)
request.predicate = pred

let mainThreadMOC = DatabaseInterface.sharedInstance.mainThreadManagedObjectContext()
fetchedResultsController = NSFetchedResultsController(fetchRequest: request, managedObjectContext: mainThreadMOC, sectionNameKeyPath: "sectionTitle", cacheName: nil)
fetchedResultsController.delegate = self

do {
    try fetchedResultsController.performFetch()
} catch {
    fatalError("Failed to initialize FetchedResultsController: \(error)")
}