Implementing NSFetchedResultsController

NSFetchedResultsController is a helper object that specializes in fetching data and managing this data. It listens to changes in its managed object context and notifies a delegate whenever the data it has fetched changes. This is incredibly helpful because it allows you to respond to specific changes in the dataset rather than reloading the table view entirely.

Being a delegate for the fetched results controller involves the following four important methods:

  • controllerWillChangeContent(_:)
  • controllerDidChangeContent(_:)
  • controller(_:didChange:at:for:newIndexPath:)
  • controller(_:didChange:atSectionIndex:for:)

The first method, controllerWillChangeContent(_:), is called right before the controller passes updates to the delegate. If you're using a table view with a fetched-results controller, this is the perfect method to begin updating the table view.

Next, controller(_:didChange:at:for:newIndexPath:) and controller(_:didChange:atSectionIndex:for:) are called to inform the delegate about updates to the fetched items and sections, respectively. This is where you should handle updates in the fetched data. For instance, you could insert new rows in a table view if new items were inserted in the dataset.

Finally, controllerDidChangeContent(_:) is called. This is the point where you should let the table view know you've finished processing the updates so all the updates can be applied to the table view's interface.

For MustC, it doesn't make sense to implement all four methods because the table view that shows family members only has a single section. This means controller(_:didChange:atSectionIndex:for:) does not have to be implemented.

To use a fetched-results controller to fetch the stored family members, you need to create an instance of NSFetchedResultsController and assign FamilyMembersViewController as its delegate so it can respond to changes in the underlying data. You can then implement the delegate methods so you can respond to changes in the fetched-results dataset. Remove the familyMembers array from the variable declarations in FamilyMembersViewController and add the following fetchedResultsController property:

var fetchedResultsController: NSFetchedResultsController<FamilyMember>? 

The viewDidLoad method should be adjusted as follows:

override func viewDidLoad() {
  super.viewDidLoad()

  let moc = persistentContainer.viewContext
  let request = NSFetchRequest<FamilyMember>(entityName: "FamilyMember")

  request.sortDescriptors = [NSSortDescriptor(key: "name", ascending: true)]
  fetchedResultsController = NSFetchedResultsController(fetchRequest: request, managedObjectContext: moc, sectionNameKeyPath: nil, cacheName: nil)

  fetchedResultsController?.delegate = self

  do {
    try fetchedResultsController?.performFetch()
  } catch {
    print("fetch request failed")
  }
}

This implementation initializes NSFetchedResultsController, assigns its delegate, and tells it to execute the fetch request. Note that the sortDescriptors property of the fetch request is set to an array that contains NSSortDescriptor. A fetched-request controller requires this property to be set, and for the list of family members, it makes sense to order family members by name.

Now that you have a fetched-results controller, you should implement the delegate methods on FamilyMembersViewController and make it conform to NSFetchedResultsControllerDelegate. Add the following extension to FamilyMembersViewController.swift:

extension FamilyMembersViewController: NSFetchedResultsControllerDelegate {
  func controllerWillChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
    tableView.beginUpdates()
  }

  func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
    tableView.endUpdates()
  }
}

The implementation for this extension is fairly straightforward. The table view gets notified when the fetched-result controller is about to process changes to its data and when the fetched-results controller is done processing changes. The bulk of the work needs to be done in controller(_:didChange:at:for:newIndexPath). This method is called when an update has been processed by the fetched-result controller. In MustC, the goal is to update a table view, but you could also update a collection view or store all of the updates in a list and do something else with them.

Let's take a look at how you can process changes to fetched data in the following method:

func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {

  switch type {
  case .insert:
    guard let insertIndex = newIndexPath
      else { return }
    tableView.insertRows(at: [insertIndex], with: .automatic)
  case .delete:
    guard let deleteIndex = indexPath
      else { return }
    tableView.deleteRows(at: [deleteIndex], with: .automatic)
  case .move:
    guard let fromIndex = indexPath,
      let toIndex = newIndexPath
      else { return }
    tableView.moveRow(at: fromIndex, to: toIndex)
  case .update:
    guard let updateIndex = indexPath
      else { return }
    tableView.reloadRows(at: [updateIndex], with: .automatic)
  }
} 

This method contains quite a lot of code, but it's actually not that complex. The preceding method receives a type parameter. This parameter is an NSFetchedResultsChangeType that contains information about the kind of update that was received. The following are the four types of updates that can occur:

  • insert
  • delete
  • move
  • update

Each of these change types corresponds to a database action. If an object was inserted, you will receive an insert change type. The proper way to handle these updates for MustC is to simply pass them on to the table view. Once all updates are received, the table view will apply all of these updates at once.

If you had implemented controller(_:didChange:atSectionIndex:for:) as well, it would also have received a change type; however, the sections only deal with the following two types of changes:

  • Insert
  • Delete

Sections don't update or move, so if you implement this method, you don't have to account for all cases because you won't encounter any, other than the two listed types of changes.

If you take a close look at the implementation for controller(_:didChange:at:for:newIndexPath), you'll notice that it receives two index paths. One is named indexPath, and the other is named newIndexPath. They're both optional, so you will need to make sure that you safely unwrap them if you use them. For new objects, only the newIndexPath property will be present. For delete and update, the indexPath property will be set. When an object is moved from one place in the dataset to another, both newIndexPath and indexPath will have a value.

The last thing you need to do is update the code in FamilyMembersViewController so it uses the fetched results controller instead of the familyMembers array that it used earlier. First, update the prepare(for:sender:) method as follows:

if let moviesVC = segue.destination as? MoviesViewController,
  let familyMember = fetchedResultsController?.object(at: selectedIndex) {
  moviesVC.persistentContainer = persistentContainer
  moviesVC.familyMember = familyMember
}

This makes sure that a valid family member is passed to the movies view controller. Update the table view data source methods as shown in the following code. A fetched-results controller can retrieve objects based on an index path. This makes it a great fit to use in combination with table views and collection views:

func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
  return fetchedResultsController?.fetchedObjects?.count ?? 0
}

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
  guard let cell = tableView.dequeueReusableCell(withIdentifier: "FamilyMemberCell"),
    let familyMember = fetchedResultsController?.object(at: indexPath)
    else { fatalError("Wrong cell identifier requested") }


  cell.textLabel?.text = familyMember.name

  return cell
}

If you run your app now, the interface updates automatically when you add a new family member to the database. However, the list of favorite movies doesn't update yet. That page does not use a fetched-results controller so it must listen to changes to the dataset directly.

The reason MoviesViewController doesn't use a fetched-results controller for the movie list is that fetched-result controllers will always need to drop down all the way to your persistent store (SQLite in this app). As mentioned before, querying the database has a significant memory overhead compared to traversing the relationship between family members and their movies; it's much faster to read the movies property than fetching them from the database.

Whenever a managed object context changes, a notification is posted to the default NotificationCenterNotificationCenter is used to send events inside of an app so other parts of the code can react to those events.

It can be very tempting to use notifications instead of delegates, especially if you're coming from a background that makes heavy use of events such as JavaScript. Don't do this; delegation is better-suited to most cases, and it will make your code much more maintainable. Only use notifications if you don't care who's listening to your notifications or if setting up a delegate relationship between objects would mean you'd create very complex relationships between unrelated objects just to set up the delegation.

Let's subscribe MoviesViewController to changes in the managed object context so it can respond to data changes if needed. Before you implement this, add the following method, which should be called when changes in the managed object context occur:

extension MoviesViewController {
  @objc func managedObjectContextDidChange(notification: NSNotification) {
    guard let userInfo = notification.userInfo,
      let updatedObjects = userInfo[NSUpdatedObjectsKey] as? Set<FamilyMember>,
      let familyMember = self.familyMember
      else { return }

    if updatedObjects.contains(familyMember) {
      tableView.reloadData()
    }
  }
} 

This method reads the notification's userInfo dictionary to access the information that's relevant to the current list. You're interested in changes to the current familyMember because when this object changes, you can be pretty sure that a new movie was just inserted. The userInfo dictionary contains keys for the inserted, deleted, and updated objects. In this case, you should look for the updated objects because users can't delete or insert new family members in this view. If the family member was updated, the table view is reloaded so it shows the new data.

The following code subscribes MoviesViewController to changes in the persistent container's managed object context:

override func viewDidLoad() {
  super.viewDidLoad()

  NotificationCenter.default.addObserver(self, selector: #selector(self.managedObjectContextDidChange(notification:)), name: .NSManagedObjectContextObjectsDidChange, object: nil)
}

When the view loads, the current MoviesViewController instance is added as an observer to the .NSManagedObjectContextObjectsDidChange notification. Go ahead and build your app; you should now see the user interface update whenever you add new data to your database.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.191.13.255