Cleaning up Core Data Fetch Requests

Core Data got a number of usability improvements in iOS 10. I have already written about NSPersistentContainer and Model Code Generation. In this post I round-up some other changes in iOS 10 and some quick tips you can use to clean up common Core Data code cluttering your view controllers.

So Much Boilerplate

Creating a fetch request is a common operation in Core Data. Assume I have a managed object of type Country and I want to fetch a sorted array of visited countries. I might create a fetch request like this:

let request = NSFetchRequest<Country>(entityName: "Country")
let sortDescriptor = NSSortDescriptor(key: "name", ascending: true)
request.sortDescriptors = [sortDescriptor]
request.predicate = NSPredicate(format: "visited == true")

If you are not careful this type of boilerplate code, full of string literals, will clutter your view controllers.

Fetch Request Convenience

New in iOS 10, NSManagedObject has an instance method which returns a generic fetch request. This is a first step in cleaning up our fetch request code. From the header file:

open class func fetchRequest() -> NSFetchRequest<NSFetchRequestResult>

A new fetch request initialized with the Entity represented by this subclass. This property’s getter is only legal to call on subclasses of NSManagedObject that represent a single entity in the model.

Unfortunately when you use it on a subclass of NSManagedObject you must give the type to avoid an error from Xcode that the fetchRequest is ambiguous:

let request: NSFetchRequest<Country> = Country.fetchRequest()

There is no nothing new here and the result is not much shorter than the original. If you have Xcode 8 and create the model subclasses you can see the implementation is a simple wrapper around the old API:

// Country+CoreDataProperties.swift
extension Country {
  @nonobjc public class func fetchRequest() -> NSFetchRequest<Country> {
    return NSFetchRequest<Country>(entityName: "Country");
  }
  // ...
}

A small improvement that at least keeps the string constants from spreading through your view controller code.

Cleaning Up Common Fetch Requests

To clean up some more I like to move common fetch request configurations to an extension/category on the managed object subclass. Let’s start with a default sort descriptor for our Country that sorts on the name:

extension Country {
  static var defaultSortDescriptors: [NSSortDescriptor] {
    return [NSSortDescriptor(key: "name", ascending: true)]
  }

We can now define a sorted fetch request:

  static var sortedFetchRequest: NSFetchRequest<Country> {
    let request: NSFetchRequest<Country> = Country.fetchRequest()
    request.sortDescriptors = Country.defaultSortDescriptors
    return request
  }

Our original fetch request for sorted visited countries is then one more step built on the sorted request:

  static var visitedFetchRequest: NSFetchRequest<Country> {
    let request = Country.sortedFetchRequest
    request.predicate = NSPredicate(format: "visited == true")
    return request
  }
}

Our view controller code is now a single line:

let fetchRequest = Country.visitedFetchRequest()

Repeat for other common fetch request configurations and remove the clutter from your controllers.

Compounding Predicates

Sometimes it is useful to combine the predicates of a fetch request. For example if I want to query the names of visited countries. I already have a predicate for finding visited countries:

NSPredicate(format: "visited == true")

I want to combine (logical AND) the above predicate with this predicate:

NSPredicate(format: "%K BEGINSWITH[cd] %@", "name", query)

You can logically AND, OR or NOT predicates using NSCompoundPredicate. This suggests another simple extension on NSFetchRequest to set or AND the predicate of a fetch request with a new predicate:

extension NSFetchRequest {
  func andPredicate(predicate: NSPredicate) {
    guard let currentPredicate = self.predicate else {
        self.predicate = predicate
        return
    }
    self.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [currentPredicate, predicate])
  }
}

We can then write:

let fetchRequest = Country.visitedFetchRequest()
let predicate = NSPredicate(format: "%K BEGINSWITH[cd] %@", "name", query)
fetchRequest.andPredicate(predicate: predicate)

Creating a New Object

I’ll finish with a convenience Apple added to iOS 10 for creating new objects in a managed object context. Instead of writing this horror:

let country = NSEntityDescription.insertNewObject(forEntityName: "Country",
  into: context) as! Country

In iOS 10 you can do this on the managed object subclass:

let country = Country(context: context)

Which I think you will agree is a big improvement.

Further Reading