Filtering Searches With NSPredicate L5.4.2

NSPredicate

This is a class from back in the Objective-C days when block which are Objective-C's version of closures were not available. But NSPredicate still has it's place in iOS with Core Data.

NSPredicate allows us to define filters that select specific elements of an array using a simple language called "predicate language".

Example Of Filtering

Below we have a pretty standard CoreDataTableViewController

import CoreData


class NotesViewController: CoreDataTableViewController {

    // MARK: TableView Data Source

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

        // Get the note
        let note = fetchedResultsController?.object(at: indexPath) as! Note

        // Get the cell
        let cell = tableView.dequeueReusableCell(withIdentifier: "Note", for: indexPath)

        // Sync note -> cell
        cell.textLabel?.text = note.text

        // Return the cell
        return cell
    }
}

Where it gets interesting is in the prepare(for:sender:) method in the NotebooksViewController which is also a subclass of CoreDataTableViewController

First we create a NSFetchRequest in Mark 1.

Next we specify how we would like the instances of Note that will be returned by NSFetchRequest to be sorted in Mark 2 - we go with creation date and by text.

To filter down to only the notes that belong to the notebook we will be segueing to we use NSPredicate (which can be configures with a micro-language call predicate query language).

In order to create the NSPredicate we need to find the exact notebook we'll be segueing to so we find the indexPath for the selected row and use it to get the correct notebook in Mark 3.

The easiest way to create an NSPredicate is with the initializer that takes a format string and an array of arguments that will be inserted into the format string, which we do in Mark 4. When instantiating the NSPredicate we use a string "notebook = %@", the %@ is a placeholder for an object, in our case a Notebook

Then we add the newly created NSPredicate to our `NSFetchRequest in Mark 5.

After that we create the FetchResultsController in Mark 6.

And lastly we inject the FetchResultsController into the notes view controller in Mark 7 so we can display it.

import CoreData

class NotebooksViewController: CoreDataTableViewController {

    // MARK: Life Cycle

    override func viewDidLoad() {
        super.viewDidLoad()

        // Set the title
        title = "CoolNotes"

        // Get the stack
        let delegate = UIApplication.shared.delegate as! AppDelegate
        let stack = delegate.stack

        // Create a fetch request
        let fr = NSFetchRequest<NSFetchRequestResult>(entityName: "Notebook")
        fr.sortDescriptors = [NSSortDescriptor(key: "name", ascending: true),
                              NSSortDescriptor(key: "creationDate", ascending: false)]

        // Create the FetchedResultsController
        fetchedResultsController = NSFetchedResultsController(fetchRequest: fr, managedObjectContext: stack.context, sectionNameKeyPath: nil, cacheName: nil)
    }

    // MARK: Actions

    @IBAction func addNewNotebook(_ sender: AnyObject) {
        // Create a new notebook... and Core Data takes care of the rest!
        let nb = Notebook(name: "New Notebook", context: fetchedResultsController!.managedObjectContext)
        print("Just created a notebook: \(nb)")
    }

    // MARK: TableView Data Source

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

        // This method must be implemented by our subclass. There's no way
        // CoreDataTableViewController can know what type of cell we want to
        // use.

        // Find the right notebook for this indexpath
        let nb = fetchedResultsController!.object(at: indexPath) as! Notebook

        // Create the cell
        let cell = tableView.dequeueReusableCell(withIdentifier: "NotebookCell", for: indexPath)

        // Sync notebook -> cell
        cell.textLabel?.text = nb.name
        cell.detailTextLabel?.text = String(format: "%d notes", nb.notes!.count)

        return cell
    }

    // MARK: Navigation

    // In a storyboard-based application, you will often want to do a little preparation before navigation
    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        // Get the new view controller using segue.destinationViewController.
        // Pass the selected object to the new view controller.

        if segue.identifier! == "displayNote" {

            if let notesVC = segue.destination as? NotesViewController {

                // Mark 1 - Create Fetch Request
                let fr = NSFetchRequest<NSFetchRequestResult>(entityName: "Note")
                // Mark 2 - Specify how to sort the notebooks
                fr.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false),NSSortDescriptor(key: "text", ascending: true)]
                // Mark 3
                let indexPath = tableView.indexPathForSelectedRow!
                let notebook = fetchedResultsController?.object(at: indexPath)

                // Mark 4
                let pred = NSPredicate(format: "notebook = %@", argumentArray: [notebook!])

                // Mark 5 - Add the Predicate to the Fetch Request
                fr.predicate = pred

                // Mark 6 - Create FetchedResultsController
                let fc = NSFetchedResultsController(fetchRequest: fr, managedObjectContext:fetchedResultsController!.managedObjectContext, sectionNameKeyPath: nil, cacheName: nil)

                // Mark 7 - Inject it into the notesVC
                notesVC.fetchedResultsController = fc
            }
        }
    }
}