Updated July 12, 2015.
Today we’ll be continuing our series on Core Data in RubyMotion, discussing table view optimization of large amounts of data in your RubyMotion application. If you’ve missed the earlier posts, you can find them here:
Introduction to Core Data in Motion
Core Data Basics in RubyMotion
Core Data Relationships in RubyMotion
Core Data Pre-loading in RubyMotion
Core Data Load Optimization in RubyMotion
Once again, we turn to Ray Wenderlich for inspiration and instruction. His
Core Data tutorial wraps up with a post on the usage of
NSFetchedResultsController
, so we should probably talk about that as well.
Why do we want to use
NSFetchedResultsController
, anyway? What’s so special about it? When we started this series, with a relatively small sample dataset, it didn’t really need much optimization. Now that we’ve loaded our database up with all 244,292 wells, it definitely needs some help, because I don’t want my customers to wait
minutes for the table view to load, which is what it does at this point. That is what I would call a fetch
FAIL.
Source: http://sadmoment.com/dog-meets-tree-while-playing-fetch-in-the-park-with-a-frisbee/
We will need to reduce memory overhead, and improve the response time of our table view, now that we have all that data. Ideally, in a table view, we would only load up the data that is actually visible to the user at any given moment. And
that is exactly what the utility class
NSFetchedResultsController
provides. Let’s see how that is accomplished in RubyMotion.
First of all, we create an
NSFetchedResultsController
. Since this object requires access to the
NSManagedObjectContext
, which is in our store class, that’s where we will put it.
def fetched_results_controller
fetch_request = NSFetchRequest.alloc.init
fetch_request.entity = NSEntityDescription.entityForName('FailedBankInfo', inManagedObjectContext:@context)
sort = NSSortDescriptor.alloc.initWithKey("details.close_date", ascending: false)
fetch_request.sortDescriptors = [sort]
fetch_request.fetchBatchSize = 20
NSFetchedResultsController.alloc.initWithFetchRequest(fetch_request,
managedObjectContext:@context,
sectionNameKeyPath:nil,
cacheName:"Root")
end
The key to the construction of the
NSFetchedResultsController
is providing a base
NSFetchRequest
. This request needs to know which entity (a.k.a. model) is being fetched, and also requires an
NSSortDescriptor
so it knows in what order to return the requested objects. The
fetchBatchSize simply limits the number of objects returned on any single query to the database.
Now that we can create our
NSFetchedResultsController
, where do we call it? In this case, we will be creating it in our table view controller’s
viewDidLoad
method.
def viewDidLoad
super
error_ptr = Pointer.new(:object)
@fetch_controller = FailedBankStore.shared.fetched_results_controller
@fetch_controller.delegate = self
unless @fetch_controller.performFetch(error_ptr)
raise "Error when fetching banks: #{error_ptr[0].description}"
end
end
Here we create the
NSFetchedResultsController
, set it’s delegate to be self, and trigger the initial fetch to populate table view.
Next, we need to update the table view, so that it knows to get it’s data from the
NSFetchedResultsController
.
def tableView(tableView, numberOfRowsInSection:section)
@fetch_controller.sections.objectAtIndex(section).numberOfObjects
end
def configureCell(cell, atIndexPath:index)
bank = @fetch_controller.objectAtIndexPath(index)
cell.textLabel.text = bank.name
cell.detailTextLabel.text = "#{bank.city}, #{bank.state}"
return cell
end
CellID = 'CellIdentifier'
def tableView(tableView, cellForRowAtIndexPath:indexPath)
cell = tableView.dequeueReusableCellWithIdentifier(CellID) || UITableViewCell.alloc.initWithStyle(UITableViewCellStyleSubtitle, reuseIdentifier:CellID)
configureCell(cell, atIndexPath:indexPath)
end
These methods translate over from Ray’s tutorial pretty much intact, without much change, other than the “rubyization”.
I did sort of skip a step back there, so let’s not forget about that. In
viewDidLoad
we set the
NSFetchedResultsController
’s delegate to be self. Now, we have to implement the
NSFetchedResultsControllerDelegate
’s signature methods. Ray simply copied his implementation from an Apple sample. And I’ve simply converted his code into a Ruby module.
module NSFetchedResultsControllerDelegate
def controllerWillChangeContent(controller)
self.tableView.beginUpdates
end
def controller(controller, didChangeObject:object, atIndexPath:path, forChangeType:type, newIndexPath:new_path)
tableView = self.tableView
case type
when NSFetchedResultsChangeInsert
tableView.insertRowsAtIndexPaths([new_path], withRowAnimation:UITableViewRowAnimationFade)
when NSFetchedResultsChangeDelete
tableView.deleteRowsAtIndexPaths([path], withRowAnimation:UITableViewRowAnimationFade)
when NSFetchedResultsChangeUpdate
configureCell(tableView.cellForRowAtIndexPath(path), atIndexPath:path)
when NSFetchedResultsChangeMove
tableView.deleteRowsAtIndexPaths([path], withRowAnimation:UITableViewRowAnimationFade)
tableView.insertRowsAtIndexPaths([new_path], withRowAnimation:UITableViewRowAnimationFade)
end
end
def controller(controller, sectionIndexTitleForSectionName:sectionName)
end
def controller(controller, didChangeSection:section, atIndex:index, forChangeType:type)
case type
when NSFetchedResultsChangeInsert
self.tableView.insertSections( NSIndexSet.indexSetWithIndex(index), withRowAnimation:UITableViewRowAnimationFade)
when NSFetchedResultsChangeDelete
self.tableView.deleteSections( NSIndexSet.indexSetWithIndex(index), withRowAnimation:UITableViewRowAnimationFade)
end
end
def controllerDidChangeContent(controller)
self.tableView.endUpdates
end
end
Then we must include that module in our table view controller, to satisfy the requirements of the delegate:
class FailedBankTableViewController < UITableViewController
include NSFetchedResultsControllerDelegate
It looks like a lot of code, but if you only need to display data, and you don’t need to change it much, you should just be able to reuse this module when required.
And that, as they say, is that. We now have a working implementation of
NSFetchedResultsController
, and the data will only be loaded 20 objects at a time. This speeds things up immensely, and reduces memory usage in our app from get-killed-immediately to just fine ;-) The complete
example can be downloaded and run. Alas, I am unable to provide the “large data load” that I used, as that data is not mine to give away. I encourage you to come up with your own large data set, and plug it in, and see how it works.
What’s next after this? Since one of the strengths of Ruby (and thus RubyMotion) is it’s rich eco-system of gems, we should take all a look at what is available for use with Core Data.
Until then…
If you found this post enlightening, you will find the ebook I wrote on these topics (and more) will allow you to spend more time working on your iOS application features, instead of fighting with Core Data for hours (or days).