Reacting to database changes

In its current state, our app doesn't update its interface when a new managed object is persisted. One proposed solution for this was to manually reload the table right after we insert a new family member. Although this might work well for some time, it's not the best solution to this problem. If our application grows, we might add some functionality that enables us to import new family members from the network. Manually refreshing the table view would be problematic because our networking logic should not be aware of the table view. Luckily, there is a better suited solution to react to changes in your data.

First, we'll implement a fetched results controller to update our list of family members. Next, we'll listen to notifications in order to update the list of a family member's favorite movies.

Implementing an NSFetchedResultsController

The NSFetchedResultsController class notifies a delegate whenever it fetches data changes. This means that you won't have to worry about manually reloading the table and you'll simply process whatever updates the fetched results controller passes on.

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 will pass 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 the updates in the data. For instance, you could insert new rows in a table view.

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 the FamilyMovies application, we will implement the first three methods because our table view doesn't have any sections. The first thing we will do is create the NSFetchedResultsController and assign the FamilyMembersViewController as its delegate. Then, we can implement the delegate methods so we get notified about changes to the results we fetched. Next, remove the familyMembers array from the variable declarations and add the following fetchedResultsController property:

var fetchedResultsController: NSFetchedResultsController<FamilyMember>? 

The viewDidLoad method should be adjusted as follows:

override fun cviewDidLoad() { 
super.viewDidLoad() 
 
    guard let moc = managedObjectContext 
        else { return } 
 
    let request = NSFetchRequest<FamilyMember>(entityName:
      "FamilyMember") 
    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 the NSFetchedResultsController, assigns the delegate, and tells it to execute the fetch request.

Now that we have a fetched results controller, we should implement the delegate methods and add conformance to NSFetchedResultsControllerDelegate. We'll do this in an extension in the FamilyMembersViewController. We're using an extension because it makes our code easier to browse; any code in our extension will relate to the delegate methods we will implement, creating logical groups of code.

Start off by defining the extension inside of the FamilyMembersViewController.swift file, as follows:

extension FamilyMembersViewController: 
  NSFetchedResultsControllerDelegate { 
 
} 

We don't have to add any code to set up our delegate, that's because all delegate methods in the NSFetchedResultsControllerDelegate protocol are optional. Let's implement the controllerWillChangeContent(_:) and controllerDidChangeContent(_:) methods first; these are the simplest methods we will need to implement:

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

These implementations are fairly straightforward. We will simply notify the table view whenever updates will occur and when we're done updating. The bulk of our work needs to be done in controller(_:didChange:at:for:newIndexPath). This method is responsible for receiving and processing the updates. In our app, we simply want 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 it.

Let's take a look at how we can process these changes in the following method:

func controller(_ controller: 
  NSFetchedResultsController<NSFetchRequestResult>, 
  didChangeanObject: AnyObject, 
            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 too bad. The preceding method receives a type parameter. This parameter is an NSFetchedResultsChangeType that contains information about the kind of update we received. The following are the four types of updates available for objects:

  • Insert
  • Delete
  • Move
  • Update

Each of these change types correspond with a database action. If an object was inserted, we'll receive an insert change type. The proper way to handle these updates for our app 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.

Had we implemented the controller(_:didChange:atSectionIndex:for:) method, we 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 we receive 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 deletion and updates, the indexPath property will be set. When an object is moved around, both properties will contain a value.

The last thing we will need to do is update our app so it uses the fetched results controller instead of the familyMembers array we used earlier. First, update the prepare(for:sender:) method as follows:

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

This makes sure that we're passing a valid family member to the movies view controller. Update the table view data source methods as shown in the next code. A fetched results controller can retrieve objects based on an index path. This makes it even more compatible to use in combination with table views and collection views.

Finally, update the table view data source methods as follows; the important changes are highlighted and should speak for themselves:

func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) ->Int { 
    return fetchedResultsController?.fetchedObjects?.count ?? 0 
} 
 
func tableView(_ tableView: UITableView, cellForRowAtindexPath: IndexPath) ->UITableViewCell { 
    guard let cell = tableView.dequeueReusableCell(withIdentifier: "FamilyMemberCell") 
        else { fatalError("Wrong cell identifier requested") } 
 
    guard let familyMember = fetchedResultsController?.object(at: indexPath) 
        else { return cell } 
 
    cell.textLabel?.text = familyMember.name 
 
    return cell 
} 

If you run your app now, the interface will automatically update whenever you add a new family member to the database. However, the movie listing doesn't update yet. Also, since we're not using a fetched results controller there, we'll need to figure out some other way to update the interface if we're inserting new movies.

The reason you shouldn'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). This has a large memory overhead compared to traversing the relationship between family members and their movies; it's much faster to read the favoriteMovies property than fetching them from the database.

Whenever a managed object context changes, a notification is posted to the default NotificationCenter. The NotificationCenter is used to fire events inside of your app that different parts of your code can listen to.

Note

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 far more suited for many cases and it will make your code much more maintainable.

Only use notifications if you don't really care about 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 our MoviesViewController to the NSManagedObjectContextDidChangeNotification so it can respond to data changes if needed. Before we add the actual subscription, we will implement the method that handles the updates we receive:

extension MoviesViewController { 
  func mangedObjectContextDidChange(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 our code. We're interested in changes to the familyMember property because whenever this object changes we can be pretty sure that a new movie just inserted. The userInfo dictionary contains keys for the inserted, deleted, and updated objects. In our case, we will need to look for the updated objects because we can't delete or insert new family members on this view. If our family member was updated, we reload the table view in order to display the newly added movie.

Let's subscribe to the managed object context did change notification so we can see this all in action. Add the following implementations to MoviesViewController:

override func viewDidLoad() { 
    super.viewDidLoad() 
 
    let center = NotificationCenter.default() 
center.addObserver(self, selector: #selector(self.mangedObjectContextDidChange(notification:)), name: Notification.Name.NSManagedObjectContextObjectsDidChange, 
                           object: nil) 
  } 
 
  deinit { 
      let center = NotificationCenter.default() 
      center.removeObserver(self) 
    }  

When the view loads, we add ourselves as an observer for the NSManagedObjectContextObjectsDidChange notification and tell it to execute the change handler we just implemented. When this view controller is destroyed, we have to make sure that we remove ourselves as observers from the notification center to avoid a retain cycle.

Go ahead and build your app, you should now be able to see your 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
3.143.0.157