NSFetchedResultsController Core Data Swift 3

Why Use NSFetchedResultsController?

So far, we’re at exactly the same point we were using the SQLite3 method. However, we didn’t have to write nearly as much code (notice the absence of a FailedBankDatabase class constructing raw SQL statements), and adding other functionality such as insert/delete operations would be much simpler.

However, there’s one notable thing that we could add pretty easily with Core Data that could give us huge benefits to performance: use NSFetchedResultsController.

Right now we’re loading all of the FailedBankInfo objects from the database into memory at once. That might be fine for this app, but the more data we have the slower this will be, and could have a detrimental impact to the user.

Ideally we’d like to load only a subset of the rows, based on what the user is currently looking at in the table view. Luckily, Apple has made this easy for us by providing a great utility class called NSFetchedResultsController.

So, start by opening up file and adding a new NSFetchedResultsController instead, Ok, now onto the fun part – creating our fetched results controller!

 fileprivate lazy var fetchedResultsController : NSFetchedResultsController<Messages> = {
 // Initialize Fetch Request
 let fetchRequest: NSFetchRequest<Messages> = Messages.fetchRequest()
 
 fetchRequest.predicate = NSPredicate(format: "topic.userId == %d && topic.status == %d && topic.type == %d", argumentArray: [self.ForUser, 0, 1])
 
 // Add Sort Descriptors
 let sortDescriptor = NSSortDescriptor(key: "time", ascending: true)
 fetchRequest.sortDescriptors = [sortDescriptor]
 
 // Initialize Fetched Results Controller
 let fetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: self.context, sectionNameKeyPath: nil, cacheName: nil)
 fetchedResultsController.delegate = self
 
 return fetchedResultsController
 }()

This should look pretty familiar to the code that we used to have in viewDidLoad, to create a fetch request to pull out the FailedBankInfo objects. However, there’s a lot of new stuff here so let’s discuss…

First, any time we use an NSFetchedResultsController, we need to set a sort descriptor on the fetch request. A sort descriptor is just a fancy term for an object we set up to tell Core Data how we want our results sorted.

The cool thing about sort descriptors is they are very powerful. Not only can you sort on any property of the object you are returning, but you can sort on properties of related objects – just like we see here! We want to sort the objects based on the close date in the FailedBankDetails, but still only receive the data in FailedBankInfo – and Core Data can do this!

A very important part is the next statement – to set the batch size on the fetch request to some small size. In fact, this is the very reason we want to use the fetched results controller in this case. This way, the fetched results controller will only retrieve a subset of objects at a time from the underlying database, and automatically fetch mroe as we scroll.

So once we finish tweaking the fetch request with the sort descriptor and batch size, we just create a NSFetchedRequestController and pass in the fetch request. Note it takes a few other parameters too:

  • For the managed object context, we just pass in our context.
  • The section name key path lets us sort the data into sections in our table view. We could sort the banks by State if we wanted to, for example, here.
  • The cacheName the name of the file the fetched results controller should use to cache any repeat work such as setting up sections and ordering contents.

So now that we have a method to return a fetched results controller, let’s modify our class to use it rather than our old array method. First, update viewDidLoad as follows:

//Activate fetchedResultsController
 do {
 try fetchedResultsController.performFetch()
 } catch {
 let fetchError = error as NSError
 print("Unable to Fetch Messages")
 print("\(fetchError), \(fetchError.localizedDescription)")
 }

All we do here is get a handle to our fetchedResultsController (which implicitly creates it as well) and call performFetch to retrieve the first batch of data.

Then, update numberOfRowsInSection:

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

And update cellForRowAtIndexPath like the following:

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {

let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier, for: indexPath)
//fetch message
 let message = fetchedResultsController.object(at: indexPath)

cell.LblMessage.text = message.message

return cell
}

Ok one more thing – we need to implement the delegate methods for the NSFetchedResultsController.

func controllerWillChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
 //Begin table update on will change content
 tableView.beginUpdates()
 }
 
 func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
 
 //Reload table indexpaths accorgind to the change in database
 switch type {
 
 case .insert:
 tableView.insertRows(at: [newIndexPath!], with: .fade)
 
 case .update:
 tableView.reloadRows(at: [indexPath!], with: .fade)
 
 default:
 tableView.reloadData()
 }
 
 }
 
 func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
 //End table update on did change content
 tableView.endUpdates()
 }

Compile and run your project, and it should look the same. However, if you examine the debug output you will see something very amazing…

SELECT 0, t0.Z_PK FROM ZFAILEDBANKINFO t0 LEFT OUTER JOIN
    ZFAILEDBANKDETAILS t1 ON t0.ZDETAILS = t1.Z_PK
    ORDER BY t1.ZCLOSEDATE DESC
total fetch execution time: 0.0033s for 234 rows.

SELECT 0, t0.Z_PK, t0.Z_OPT, t0.ZNAME, t0.ZSTATE, t0.ZCITY,
    t0.ZDETAILS FROM ZFAILEDBANKINFO t0 LEFT OUTER JOIN
    ZFAILEDBANKDETAILS t1 ON t0.ZDETAILS = t1.Z_PK WHERE
    t0.Z_PK IN  (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
    ORDER BY t1.ZCLOSEDATE DESC LIMIT 20
total fetch execution time: 0.0022s for 20 rows.

SELECT 0, t0.Z_PK, t0.Z_OPT, t0.ZNAME, t0.ZSTATE, t0.ZCITY,
    t0.ZDETAILS FROM ZFAILEDBANKINFO t0 LEFT OUTER JOIN
    ZFAILEDBANKDETAILS t1 ON t0.ZDETAILS = t1.Z_PK WHERE
    t0.Z_PK IN  (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
    ORDER BY t1.ZCLOSEDATE DESC LIMIT 20
total fetch execution time: 0.0017s for 20 rows.

You can see here that behind the scenes, the NSFetchedResultsController is getting a list of IDs from FailedBankInfo, in the proper sort order. Then, as the user pages through the table, it loads one batch at a time – rather than loading all of the objects into memory at once!

This would have been a lot more code to do with raw SQLite – and is just one of the many reasons why using Core Data can save time and increase performance.

Advertisements
NSFetchedResultsController Core Data Swift 3

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s