I'd like to setup an NSFetchedResultsController the way iOS Messages works, meaning, I'd like to get the most recent items to fill the screen first, then fetch as a user scrolls back historically in the tableview.
I think I have a little bias from just working with a FetchedResultsController and it's delegates like "normal" and am not quite sure how to go about it.
I'm also not sure if this is even the right implement to use for what I'd like to get :)
I just want to fetch the most recent records, display them in a table view and as a user scrolls up continue to fetch items and insert them above the existing rows.
Here's just a regular setup that I have so far:
import UIKit
import CoreData
class ViewController: UIViewController {
var coreDataStack: CoreDataStack!
@IBOutlet weak var tableView: UITableView!
@IBOutlet weak var addButton: UIBarButtonItem!
var fetchedResultsController: NSFetchedResultsController!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
let fetchRequest = NSFetchRequest(entityName: "Item")
let timestampSort = NSSortDescriptor(key: "timestamp", ascending: true)
fetchRequest.sortDescriptors = [timestampSort]
fetchedResultsController =
NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: coreDataStack.context, sectionNameKeyPath: nil, cacheName: nil)
self.fetchedResultsController.delegate = self
do {
try self.fetchedResultsController.performFetch()
} catch let error as NSError {
print("initial fetch error is: \(error.localizedDescription)")
}
}
}
extension ViewController: UITableViewDataSource {
func numberOfSectionsInTableView
(tableView: UITableView) -> Int {
return self.fetchedResultsController.sections!.count
}
func tableView(tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
let sectionInfo = fetchedResultsController.sections![section]
return sectionInfo.name
}
func tableView(tableView: UITableView,
numberOfRowsInSection section: Int) -> Int {
let sectionInfo = self.fetchedResultsController.sections![section]
return sectionInfo.numberOfObjects
}
func tableView(tableView: UITableView,
cellForRowAtIndexPath indexPath: NSIndexPath)
-> UITableViewCell {
let cell =
tableView.dequeueReusableCellWithIdentifier(
"ItemCell", forIndexPath: indexPath)
as! ItemCell
let item = self.fetchedResultsController.objectAtIndexPath(indexPath) as! Item
cell.textLabel.text = item.name
return cell
}
}
extension ViewController: NSFetchedResultsControllerDelegate {
func controllerWillChangeContent(controller: NSFetchedResultsController) {
self.tableView.beginUpdates()
}
func controller(controller: NSFetchedResultsController, didChangeObject anObject: AnyObject, atIndexPath indexPath: NSIndexPath?, forChangeType type: NSFetchedResultsChangeType, newIndexPath: NSIndexPath?) {
switch type {
case .Insert:
self.tableView.insertRowsAtIndexPaths([newIndexPath!], withRowAnimation: .Automatic)
case .Delete:
self.tableView.deleteRowsAtIndexPaths([indexPath!], withRowAnimation: .Automatic)
case .Update:
return
case .Move:
self.tableView.deleteRowsAtIndexPaths([indexPath!], withRowAnimation: .Automatic)
self.tableView.insertRowsAtIndexPaths([newIndexPath!], withRowAnimation: .Automatic)
}
}
func controllerDidChangeContent(controller: NSFetchedResultsController) {
self.tableView.endUpdates()
}
}
The NSFetchedResultsController
just stores an array of objects in sort order, which you can access via the fetchedObjects
method. Thus, to display the last X messages, you need to display the last X elements of that array.
Rather than try to calculate that in every numberOfRowsInSection()
and cellForRowAtIndexPath()
, I've found it easier to cache a copy of the X elements you're currently displaying every time the NSFetchedResultsController
changes (in controllerDidChangeContent()
). That is, in every call to controllerDidChangeContent
, you copy the last X elements from the fetched results controller's fetchedObjects
(example code in Objective-C, because that's what I used for the project where I had to do this)
@property (strong, nonatomic) NSArray *msgsToDisplay;
@property unsigned long numToDisplay;
@property unsigned long numberActuallyDisplaying;
- (void)viewDidLoad {
// ...
self.msgsToDisplay = [[NSArray alloc] init];
self.numToDisplay = 20; // or whatever count you want to display initially
// ...
}
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller
{
NSArray *allMsgs = [[_fetchedResultsController fetchedObjects] copy];
self.numberActuallyDisplaying = MIN(self.numToDisplay, [allMsgs count]);
self.msgsToDisplay = [allMsgs subarrayWithRange:NSMakeRange([allMsgs count] - self.numberActuallyDisplaying, self.numberActuallyDisplaying)];
}
Then your row count (assuming just one section in the table) is the number of messages you're actually displaying:
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return self.numberActuallyDisplaying;
}
And cellForRowAtIndexPath
can just index into your cached copy of the objects:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
Message *msg = [self.msgsToDisplay objectAtIndex:indexPath.row];
//...
}
As the user scrolls up, you can use a UIRefreshControl
(https://developer.apple.com/library/ios/documentation/UIKit/Reference/UIRefreshControl_class/) to allow the user to request more data. It looks like you're not using a UITableViewController
, so you'll need to create a UIRefreshControl
explicitly and add it to the table. In viewDidLoad()
:
UIRefreshControl *refreshControl = [[UIRefreshControl alloc] init];
[refreshControl addTarget:self action:@selector(handleRefresh:) forControlEvents:UIControlEventValueChanged];
[self.tableView insertSubview:refreshControl atIndex:0];
When the user pulls down to refresh, you can set your self.numToDisplay
to a higher number, and then update your self.msgsToDisplay
and self.numActuallyDisplaying
based on the new number to display.
- (void) handleRefresh:(UIRefreshControl *)controller
{
self.numToDisplay += NUMBER_TO_DISPLAY_INCREMENT;
__block NSArray *allMsgs;
[[_fetchedResultsController managedObjectContext] performBlockAndWait:^{
allMsgs = [[_fetchedResultsController fetchedObjects] copy];
}];
self.numberActuallyDisplaying = MIN(self.numToDisplay, [allMsgs count]);
self.msgsToDisplay = [allMsgs subarrayWithRange:NSMakeRange([allMsgs count] - self.numberActuallyDisplaying, self.numberActuallyDisplaying)];
[controller endRefreshing];
}
Converting this all to Swift should be straightforward, but let me know if you'd like help with that.