NSFetchedResultsControllerDelegate provides mechanisms for tracking changes that happen to the objects of our NSFetchRequest in the model. Let’s refer to the UITableView example to learn how to display changes that have taken place in the model while maintaining proper UI representation.For display purposes, I highlighted the fetched objects with a dotted line. As you may already know, Core Data is a framework for data storage and interaction. There is a ton of info about Core Data on the net if you want to learn more about the framework in general.
It is common for beginners (especially developers from Stack Overflow) to shy away from using the Core Data Framework because it comes across as complicated, or to employ only a minor portion of its capabilities. In practice, knowing the basic functions of this framework’s classes allows the developer to easily work with the model.
In this article, I want to focus on the following aspects:
- We will examine the NSFetchRequest class, which creates requests to fetch the existing data from the model. We will go over its basic properties and present in-use cases.
- We will take a detailed look at
NSFetchedResultsController
, its functions and how it conveniently displays fetched data with the help ofNSFetchRequest
, usingUITableView
as an example.
Demo project description
Our guinea pig will be a rather simplistic demo project that includes Model
and ViewController
that contains UITableView
. For demonstrative purposes, we will use an ordinary grocery list with names and prices.
Model
The model will contain two entities: Products with the attributes name
and price
, and FavoriteProducts
that inherits these attributes.
As an example, we’ll fill up our database with a certain number of groceries, each with a random price (up to 1000) and a product name from the following list: “Milk”
, “Soda”
, “Donut”
, “Banana”
, “Cheddar Cheese”
, “Parmesan Cheese”
, “Flour”
, “Oatmeal”
.
Controller
We’ll use the following code in the controller to initialize and display the table in full screen.
Objective-C
- (UITableView *)tableView { if (_tableView != nil) return _tableView; _tableView = [[UITableView alloc] initWithFrame:CGRectZero style:UITableViewStyleGrouped]; _tableView.backgroundColor = [UIColor whiteColor]; _tableView.dataSource = self; return _tableView; } - (void)loadView { [super loadView]; [self.view addSubview:self.tableView]; } - (void)viewDidLayoutSubviews { [super viewDidLayoutSubviews]; _tableView.frame = self.view.frame; }
Swift
var tableView: UITableView = { let tableView = UITableView(frame: CGRectZero, style: .Grouped) tableView.backgroundColor = UIColor.whiteColor() return tableView }() override func loadView() { super.loadView() self.view.addSubview(tableView) } override func viewDidLoad() { super.viewDidLoad() tableView.dataSource = self } override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() tableView.frame = self.view.frame }
Data fetching
The data is fetched from the model with the help of the executeFetchRequest(_:)
method argument. The centerpiece of our article is the NSFetchRequest
method argument.
Objective-C
NSManagedObjectContext *context = [[CoreDataManager instance] managedObjectContext]; NSEntityDescription *entityDescription = [NSEntityDescription entityForName:@"Products" inManagedObjectContext:context]; NSFetchRequest *request = [[NSFetchRequest alloc] init]; request.entity = entityDescription; NSError *error = nil; NSArray* objects = [context executeFetchRequest:request error:&error];
Swift
let context = CoreDataManager.instance.managedObjectContext let entityDescription = NSEntityDescription.entityForName("Products", inManagedObjectContext: context) let request = NSFetchRequest() request.entity = entityDescription do { let objects = try context.executeFetchRequest(request) } catch { fatalError("Failed to fetch employees: \(error)") }
By default, the type returned by the "Donut"
method is the object array of the NSManagedObject
class. For display purposes, let’s print out
the entries fetched from our model in a temporary output format.
The result
NAME: Donut, PRICE: 156 NAME: Soda, PRICE: 425 NAME: Soda, PRICE: 85 NAME: Cheddar Cheese, PRICE: 400 NAME: Flour, PRICE: 920 NAME: Parmesan Cheese, PRICE: 861 NAME: Soda, PRICE: 76 NAME: Milk, PRICE: 633 NAME: Soda, PRICE: 635 NAME: Parmesan Cheese, PRICE: 718 NAME: Donut, PRICE: 701 NAME: Soda, PRICE: 176 NAME: Banana, PRICE: 731 NAME: Parmesan Cheese, PRICE: 746 NAME: Flour, PRICE: 456 NAME: Flour, PRICE: 519 NAME: Cheddar Cheese, PRICE: 221 NAME: Flour, PRICE: 560 NAME: Parmesan Cheese, PRICE: 646 NAME: Donut, PRICE: 492 NAME: Banana, PRICE: 185 NAME: Soda, PRICE: 539 NAME: Parmesan Cheese, PRICE: 872 NAME: Banana, PRICE: 972 NAME: Donut, PRICE: 821 NAME: Milk, PRICE: 409 NAME: Banana, PRICE: 334 NAME: Milk, PRICE: 734 NAME: Soda, PRICE: 448 NAME: Parmesan Cheese, PRICE: 345
Basic methods and properties of the NSFetchRequest class
As its name suggests, the NSFetchRequest class is used to request data fetches from the model. This tool allows you to set object filtering and sorting rules at the fetching stage, so the operation becomes several times quicker and more efficient than fetching every object first (what if there are 10,000 objects or more?) and then manually sorting or filtering the data we require.
When we know the basic properties of this class, we can easily manage requests and get a specific fetch without developing additional algorithms and crutches as everything is already implemented in Core Data. Let’s get started.
sortDescriptors
Objective-C
@property (nonatomic, strong) NSArray <NSSortDescriptor *> *sortDescriptors
Swift
var sortDescriptors: [NSSortDescriptor]?
It seems appropriate to begin reviewing the properties of the class from sortDescriptors, which is an array of objects of the NSSortDescriptor class that are used by the sorting mechanism. You can find the guide on using the sort descriptors on the Apple portal. This property accepts the sort descriptors array, allowing us to use several sorting rules. In this case, priorities are equivalent to queue rules (FIFO, First In-First Out): the lower the object’s index in the array, the higher the sorting priority.
In-use example
Printing out all the objects the way we did before leads to a chaotic and hard-to-read output. For convenience, let’s sort this list: first, by grocery product name, specifying the name attribute as the sort key, and then by price. We want to sort by name in alphabetical order and by price in ascending order; for this, we have to set the ascending value of both predicates to Boolean true (Boolean false is used for descending order).
Objective-C
NSSortDescriptor *nameSortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"name" ascending:YES]; NSSortDescriptor *priceSortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"price" ascending:YES]; fetchRequest.sortDescriptors = @[nameSortDescriptor, priceSortDescriptor];
Swift
let nameSortDescriptor = NSSortDescriptor(key: "name", ascending: true) let priceSortDescriptor = NSSortDescriptor(key: "price", ascending: true) fetchRequest.sortDescriptors = [nameSortDescriptor, priceSortDescriptor]
The result of the sorting operation
NAME: Banana, PRICE: 185 NAME: Banana, PRICE: 334 NAME: Banana, PRICE: 731 NAME: Banana, PRICE: 972 NAME: Donut, PRICE: 156 NAME: Donut, PRICE: 492 NAME: Donut, PRICE: 701 NAME: Donut, PRICE: 821 NAME: Soda, PRICE: 76 NAME: Soda, PRICE: 85 NAME: Soda, PRICE: 176 NAME: Soda, PRICE: 425 NAME: Soda, PRICE: 448 NAME: Soda, PRICE: 539 NAME: Soda, PRICE: 635 NAME: Parmesan Cheese, PRICE: 345 NAME: Parmesan Cheese, PRICE: 646 NAME: Parmesan Cheese, PRICE: 718 NAME: Parmesan Cheese, PRICE: 746 NAME: Parmesan Cheese, PRICE: 861 NAME: Parmesan Cheese, PRICE: 872 NAME: Cheddar Cheese, PRICE: 221 NAME: Cheddar Cheese, PRICE: 400 NAME: Milk, PRICE: 409 NAME: Milk, PRICE: 633 NAME: Milk, PRICE: 734 NAME: Flour, PRICE: 456 NAME: Flour, PRICE: 519 NAME: Flour, PRICE: 560 NAME: Flour, PRICE: 920
predicate
Objective-C
@property (nonatomic, strong) NSPredicate *predicate
Swift
var predicate: NSPredicate?
The next property we’ll examine is predicate
of the NSPredicate
class, a quick and powerful data filtering tool. There’s an excellent guide to using predicate from Apple. Data filtering on request is done thanks to the predicate’s distinctive string syntax, described in the aforementioned guide.
In-use example
Let’s start with a simple example. We’d like to know the prices for pepperoni sausages in our grocery list. For this, we need to specify in the predicate that we want all the objects that have the "Cheddar Cheese”
string in their name attribute value.
Objective-C
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"name == %@", @"Cheddar Cheese"]; fetchRequest.predicate = predicate;
Swift
let predicate = NSPredicate(format: "name == %@", "Cheddar Cheese") fetchRequest.predicate = predicate
The result
NAME: Cheddar Cheese, PRICE: 400 NAME: Cheddar Cheese, PRICE: 221
Take note that to create a correct predicate with a == equal operator you have to specify the string value precisely as it is case sensitive.
But what if we want to check the prices for all kinds of sausages, including but not limited to pepperoni? In this case, we have to use the CONTAINS operator (left expression contains right expression) and add the [cd]
keywords that turn off case and diacritics sensitivity. We can also use several conditions with the help of AND operator. Let’s also add a 500 price limit to our results as well.
Objective-C
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"name CONTAINS[cd] %@ AND price < %d", @"колбаса", 500]; fetchRequest.predicate = predicate;
Swift
let predicate = NSPredicate(format: "name CONTAINS[cd] %@ AND price < %d", "колбаса", 500) fetchRequest.predicate = predicate
The result
NAME: Cheddar Cheese, PRICE: 400 NAME: Cheddar Cheese, PRICE: 221 NAME: Parmesan Cheese, PRICE: 345
fetchLimit
Objective-C
@property (nonatomic) NSUInteger fetchLimit
Swift
var fetchLimit: Int
The fetchLimit
property allows you to limit the amount of fetched objects.
In-use example
To demonstrate this property, let’s retrieve the 12 cheapest products from our groceries list. For this, we’ll add price sorting and set a limit of 12 to the amount of fetched objects.
Objective-C
// sorted by price NSSortDescriptor *priceSortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"price" ascending:YES]; fetchRequest.sortDescriptors = @[priceSortDescriptor]; // amount limit = 12 fetchRequest.fetchLimit = 12;
Swift
// sorted by price let priceSortDescriptor = NSSortDescriptor(key: "price", ascending: true) fetchRequest.sortDescriptors = [priceSortDescriptor] // amount limit = 12 fetchRequest.fetchLimit = 12
The result
NAME: Soda, PRICE: 76 NAME: Soda, PRICE: 85 NAME: Donut, PRICE: 156 NAME: Soda, PRICE: 176 NAME: Banana, PRICE: 185 NAME: Cheddar Cheese, PRICE: 221 NAME: Banana, PRICE: 334 NAME: Parmesan Cheese, PRICE: 345 NAME: Cheddar Cheese, PRICE: 400 NAME: Milk, PRICE: 409 NAME: Soda, PRICE: 425 NAME: Soda, PRICE: 448
fetchOffset
Objective-C
@property (nonatomic) NSUInteger fetchOffset
Swift
var fetchOffset: Int
This property can be used to shift the fetch results by a set amount of objects.
In-use example
To demonstrate how this property works, we’ll take the previous request and shift it by two objects. As a result, we’ll get 12 objects with the first 2 of them skipped and the next 2 added at the end, as if we’ve shifted the results table by two cells.
Objective-C
fetchRequest.fetchOffset = 2;
Swift
fetchRequest.fetchOffset = 2
For display purposes, I highlighted the fetched objects with a dotted line.
Before: After: NAME: Soda, PRICE: 76 NAME: Soda, PRICE: 85 --------------------------------------------------------------------------------- NAME: Soda, PRICE: 76 NAME: Donut, PRICE: 156 NAME: Soda, PRICE: 85 NAME: Soda, PRICE: 176 NAME: Donut, PRICE: 156 NAME: Banana, PRICE: 185 NAME: Soda, PRICE: 176 NAME: Cheddar Cheese, PRICE: 221 NAME: Banana, PRICE: 185 NAME: Banana, PRICE: 334 NAME: Cheddar Cheese, PRICE: 221 NAME: Parmesan Cheese, PRICE: 345 NAME: Banana, PRICE: 334 NAME: Cheddar Cheese, PRICE: 400 NAME: Parmesan Cheese, PRICE: 345 NAME: Milk, PRICE: 409 NAME: Cheddar Cheese, PRICE: 400 NAME: Soda, PRICE: 425 NAME: Milk, PRICE: 409 NAME: Soda, PRICE: 448 NAME: Soda, PRICE: 425 NAME: Flour, PRICE: 456 NAME: Soda, PRICE: 448 NAME: Donut, PRICE: 492 --------------------------------------------------------------------------------- NAME: Flour, PRICE: 456 ... NAME: Donut, PRICE: 492 ...
fetchBatchSize
Objective-C
@property (nonatomic) NSUInteger fetchBatchSize
Swift
var fetchBatchSize: Int
The fetchBatchSize
property is used to adjust the amount of objects that can be fetched at once from the database (Persistent Store
) that the Core Data Framework
is working with (SQLite
, XML
, etc). The best value to set is different for each specific case, so this property might either speed up or slow down your database performance.
Let’s say we are working with UITableView
and there are more than 10,000 objects in our model. It’ll take some time to fetch all these objects at once. But our table can only hold 20 cells per screen and we only need to display 20 objects. In this case it would be a good idea to set fetchBatchSize
to 20. At first, Core Data
will request 20 objects to display in our table from the database and will then request the next batch of 20 entries on a scroll. This approach is sure to help you optimize your persistent store interaction.
Make sure that your batch size is not too small, though. A batch size of 1 will only bog down your database with constant requests for a single entry.
Objective-C
fetchRequest.fetchBatchSize = 20;
Swift
fetchRequest.fetchBatchZize = 20
resultType
Objective-C
@property (nonatomic) NSFetchRequestResultType resultType
Swift
<pre> var resultType: NSFetchRequestResultType
When fetching data, the executeFetchRequest(_:)
method by default returns the array of objects of the NSManagedObject
class and its children.
Changing the resultType
property allows you to pick the type of fetched objects. Let’s take a look at them (Objective-C with the NS prefix is followed by Swift after a slash):
NSManagedObjectResultType
/ManagedObjectResultType
— objects of theNSManagedObject
class and its children (by default).NSManagedObjectIDResultType
/ManagedObjectIDResultType
— identifier ofNSManagedObject
.NSDictionaryResultType
/DictionaryResultType
— dictionary with keys that are entity attribute names.NSCountResultType
/CountResultType
— will return one array entry with a value of entry count.
propertiesToFetch
Objective-C
@property (nonatomic, copy) NSArray *propertiesToFetch
Swift
var propertiesToFetch: [AnyObject]?
This property allows you to fetch only the required attributes from the entity, but only under the condition that resultType
is a dictionary (NSDictionaryResultType
/ DictionaryResultType
).
In-use example
As an example, we’ll fetch only the values of the name attribute, and for output we’ll print out all the values for all the existing dictionary keys in the following format: (key: value
).
Objective-C
fetchRequest.resultType = NSDictionaryResultType; fetchRequest.propertiesToFetch = @[@"name"];
Swift
fetchRequest.resultType = .DictionaryResultType fetchRequest.propertiesToFetch = ["name"]
The result (key: value)
name: Donut name: Soda name: Soda name: Cheddar Cheese name: Flour name: Parmesan Cheese name: Soda name: Milk name: Soda name: Parmesan Cheese name: Donut name: Soda name: Banana name: Parmesan Cheese name: Flour name: Flour name: Cheddar Cheese name: Flour name: Parmesan Cheese name: Donut name: Banana name: Soda name: Parmesan Cheese name: Banana name: Donut name: Milk name: Banana name: Milk name: Soda name: Parmesan Cheese
includesSubentities
Objective-C
@property (nonatomic) BOOL includesSubentities
Swift
var includesSubentities: Bool
To demonstrate this property we’ll have to add an object into the FavoriteProducts
entity that is inherited from Products
. We’ll assign this object the name "Apple"
and a price of 999.
We’ll request the Products
entity that we used to sort by name and price.
The result
NAME: Banana, PRICE: 185 NAME: Banana, PRICE: 334 NAME: Banana, PRICE: 731 NAME: Banana, PRICE: 972 NAME: Donut, PRICE: 156 NAME: Donut, PRICE: 492 NAME: Donut, PRICE: 701 NAME: Donut, PRICE: 821 NAME: Soda, PRICE: 76 NAME: Soda, PRICE: 85 NAME: Soda, PRICE: 176 NAME: Soda, PRICE: 425 NAME: Soda, PRICE: 448 NAME: Soda, PRICE: 539 NAME: Soda, PRICE: 635 NAME: Parmesan Cheese, PRICE: 345 NAME: Parmesan Cheese, PRICE: 646 NAME: Parmesan Cheese, PRICE: 718 NAME: Parmesan Cheese, PRICE: 746 NAME: Parmesan Cheese, PRICE: 861 NAME: Parmesan Cheese, PRICE: 872 NAME: Cheddar Cheese, PRICE: 221 NAME: Cheddar Cheese, PRICE: 400 NAME: Milk, PRICE: 409 NAME: Milk, PRICE: 633 NAME: Milk, PRICE: 734 NAME: Flour, PRICE: 456 NAME: Flour, PRICE: 519 NAME: Flour, PRICE: 560 NAME: Flour, PRICE: 920 NAME: Apple, PRICE: 999
Take note of the object that we’ve just added for the FavoriteProducts
entity at the end of the list. Why is it there? By default, the request’s includesSubentities
property value is equal to Boolean true, which leads to fetching of objects of both the current entity and children entities.
To prevent this, change it to Boolean false.
Objective-C
fetchRequest.includesSubentities = NO;
Swift
fetchRequest.includesSubentities = false
The result
NAME: Banana, PRICE: 185 NAME: Banana, PRICE: 334 NAME: Banana, PRICE: 731 NAME: Banana, PRICE: 972 NAME: Donut, PRICE: 156 NAME: Donut, PRICE: 492 NAME: Donut, PRICE: 701 NAME: Donut, PRICE: 821 NAME: Soda, PRICE: 76 NAME: Soda, PRICE: 85 NAME: Soda, PRICE: 176 NAME: Soda, PRICE: 425 NAME: Soda, PRICE: 448 NAME: Soda, PRICE: 539 NAME: Soda, PRICE: 635 NAME: Parmesan Cheese, PRICE: 345 NAME: Parmesan Cheese, PRICE: 646 NAME: Parmesan Cheese, PRICE: 718 NAME: Parmesan Cheese, PRICE: 746 NAME: Parmesan Cheese, PRICE: 861 NAME: Parmesan Cheese, PRICE: 872 NAME: Cheddar Cheese, PRICE: 221 NAME: Cheddar Cheese, PRICE: 400 NAME: Milk, PRICE: 409 NAME: Milk, PRICE: 633 NAME: Milk, PRICE: 734 NAME: Flour, PRICE: 456 NAME: Flour, PRICE: 519 NAME: Flour, PRICE: 560 NAME: Flour, PRICE: 920
Fetched Results Controller (FRC)
The NSFetchedResultsController
class controller can be placed between Core Data
and ViewController
, in which we need to display data from our database. The methods and properties of this controller allow for convenient interaction, display and management of objects from Core Data
together with UITableView tables
, for which it’s best adapted.
This controller can convert fetched objects into table entries (sections and objects of these sections). FRC
has the NSFetchedResultsControllerDelegate
protocol that allows tracking changes that happen to the objects of the specified NSFetchRequest
on controller initialization.
FRC initialization
Objective-C
- (instancetype)initWithFetchRequest:(NSFetchRequest *)fetchRequest managedObjectContext: (NSManagedObjectContext *)context sectionNameKeyPath:(nullable NSString *)sectionNameKeyPath cacheName:(nullable NSString *)name;
Swift
public init(fetchRequest: NSFetchRequest, managedObjectContext context: NSManagedObjectContext, sectionNameKeyPath: String?, cacheName name: String?)
Let’s examine the initialization parameters:
fetchRequest
— request forNSFetchRequest
object fetch. Note: forFRC
to work, the request needs to have at least one sort descriptor and its resultType must beNSManagedObjectResultType
/ManagedObjectResultType
.context
— theNSManagedObjectContext
context in which we are working.sectionNameKeyPath
— an optional parameter. When provided in a string key format (entity attribute name), it groups objects with equal values of this attribute in a table section. It is important that this key matches the sort descriptor with the highest priority. If this parameter is not provided, a table with one section will be created.cacheName
— an optional parameter. When it is provided, the controller starts to cache request results; we’ll take a closer look at it later.
Our next step will be to invoke the performFetch
controller method to actually execute the fetch.
Objective-C
NSError *error = nil; if (![self.fetchedResultsController performFetch:&error]) { NSLog(@"Unresolved error %@, %@", error, [error userInfo]); abort(); }
Swift
do { try fetchedResultsController.performFetch() } catch { print(error) }
The method returns a Boolean value. A Boolean true is returned in case of a successful fetch, otherwise it’s false. After the objects are fetched, they are placed in the fetchedObjects
controller property.
Interaction with UITableView
It’s time to take a look at interaction with tables. Even though fetched objects are placed in the fetchedObject
property, to actually work with them you have to use the sections controller property. It’s an object array under the NSFetchedResultsSectionInfo
protocol that defines the following properties:
name
— section name.indexTitle
— section title.numbersOfObjects
— the number of objects in a section.objects
— the actual object array located in the section.
Implementation
For our convenience, we’ll add the configureCell
table cell configuration method.
Objective-C
#pragma mark - Table View - (void)configureCell:(UITableViewCell *)cell withObject:(Products *)object { cell.textLabel.text = object.name; cell.detailTextLabel.text = [NSString stringWithFormat:@"%d", object.price.intValue]; } #pragma mark UITableViewDataSource - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { return [[self.fetchedResultsController sections] count]; } - (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section { id <NSFetchedResultsSectionInfo> sectionInfo = [self.fetchedResultsController sections][section]; return sectionInfo.indexTitle; } - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { id <NSFetchedResultsSectionInfo> sectionInfo = [self.fetchedResultsController sections][section]; return [sectionInfo numberOfObjects]; } - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { static NSString *identifier = @"Cell"; UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:identifier]; if (!cell) { cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleValue1 reuseIdentifier:identifier]; } Products *object = [[self fetchedResultsController] objectAtIndexPath:indexPath]; [self configureCell:cell withObject:(Products *)object]; return cell; }
Swift
// MARK: - Table View extension ViewController { func configureCell(cell: UITableViewCell, withObject product: Products) { cell.textLabel?.text = product.name ?? "" cell.detailTextLabel?.text = String(product.price ?? 0) } } // MARK: UITableViewDataSource extension ViewController: UITableViewDataSource { func numberOfSectionsInTableView(tableView: UITableView) -> Int { guard let sections = fetchedResultsController.sections else { return 0 } return sections.count } func tableView(tableView: UITableView, titleForHeaderInSection section: Int) -> String? { guard let sections = fetchedResultsController.sections else { return nil } return sections[section].indexTitle ?? "" } func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int { guard let sections = fetchedResultsController.sections else { return 0 } return sections[section].numberOfObjects } func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { let identifier = "Cell" let product = fetchedResultsController.objectAtIndexPath(indexPath) as! Products var cell = tableView.dequeueReusableCellWithIdentifier(identifier) if cell == nil { cell = UITableViewCell(style: .Value1, reuseIdentifier: identifier) } configureCell(cell!, withObject: product) return cell! } }
Using NSFetchRequest
with sorting by name and price and specifying sectionNameKeyPath
for FRC
as "name"
attribute name, we get a table that groups our grocery products by name.
The result
FRC can function in several modes:
- The controller doesn’t have a delegate and no cache name is specified (
delegate = nil
,cacheName = nil
) — in this mode, the data is retrieved only on fetch request and is not cached. - The delegate is assigned but there’s no cache name (
delegate != nil
,cacheName = nil
) — change tracking mode that uses theNSFetchedResultsControllerDelegate
protocol method (we’ll describe it later). Still no object caching. - Both delegate and cache name are set (
delegate != nil
,cacheName = <#NSString/String#>
) — full tracking mode with object caching.
NSFetchedResultsControllerDelegate
NSFetchedResultsControllerDelegate
provides mechanisms for tracking changes that happen to the objects of our NSFetchRequest
in the model. Let’s refer to the UITableView
example to learn how to display changes that have taken place in the model while maintaining proper UI representation.
Objective-C
#pragma mark - NSFetchedResultsControllerDelegate // 1 - (NSString *)controller:(NSFetchedResultsController *)controller sectionIndexTitleForSectionName:(NSString *)sectionName { return sectionName; } // 2 - (void)controllerWillChangeContent:(NSFetchedResultsController *)controller { [self.tableView beginUpdates]; } // 3 - (void)controller:(NSFetchedResultsController *)controller didChangeSection:(id <NSFetchedResultsSectionInfo>)sectionInfo atIndex:(NSUInteger)sectionIndex forChangeType:(NSFetchedResultsChangeType)type { switch(type) { case NSFetchedResultsChangeInsert: [self.tableView insertSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade]; break; case NSFetchedResultsChangeDelete: [self.tableView deleteSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade]; break; default: return; } } // 4 - (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(NSIndexPath *)newIndexPath { UITableView *tableView = self.tableView; switch(type) { case NSFetchedResultsChangeInsert: [tableView insertRowsAtIndexPaths:@[newIndexPath] withRowAnimation:UITableViewRowAnimationFade]; break; case NSFetchedResultsChangeDelete: [tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationFade]; break; case NSFetchedResultsChangeUpdate: [self configureCell:[tableView cellForRowAtIndexPath:indexPath] withObject:anObject]; break; case NSFetchedResultsChangeMove: [tableView moveRowAtIndexPath:indexPath toIndexPath:newIndexPath]; break; } } // 5 - (void)controllerDidChangeContent:(NSFetchedResultsController *)controller { [self.tableView endUpdates]; }
Swift
// MARK: - NSFetchedResultsControllerDelegate extension ViewController: NSFetchedResultsControllerDelegate { // 1 func controller(controller: NSFetchedResultsController, sectionIndexTitleForSectionName sectionName: String) -> String? { return sectionName } // 2 func controllerWillChangeContent(controller: NSFetchedResultsController) { tableView.beginUpdates() } // 3 func controller(controller: NSFetchedResultsController, didChangeSection sectionInfo: NSFetchedResultsSectionInfo, atIndex sectionIndex: Int, forChangeType type: NSFetchedResultsChangeType) { switch type { case .Insert: tableView.insertSections(NSIndexSet(index: sectionIndex), withRowAnimation: .Fade) case .Delete: tableView.deleteSections(NSIndexSet(index: sectionIndex), withRowAnimation: .Fade) default: return } } // 4 func controller(controller: NSFetchedResultsController, didChangeObject anObject: AnyObject, atIndexPath indexPath: NSIndexPath?, forChangeType type: NSFetchedResultsChangeType, newIndexPath: NSIndexPath?) { switch type { case .Insert: if let indexPath = newIndexPath { tableView.insertRowsAtIndexPaths([indexPath], withRowAnimation: .Automatic) } case .Update: if let indexPath = indexPath { let product = fetchedResultsController.objectAtIndexPath(indexPath) as! Products guard let cell = tableView.cellForRowAtIndexPath(indexPath) else { break } configureCell(cell, withObject: product) } case .Move: if let indexPath = indexPath { tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Automatic) } if let newIndexPath = newIndexPath { tableView.insertRowsAtIndexPaths([newIndexPath], withRowAnimation: .Automatic) } case .Delete: if let indexPath = indexPath { tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Automatic) } } } // 5 func controllerDidChangeContent(controller: NSFetchedResultsController) { tableView.endUpdates() } }
Delegate methods and their usage:
sectionIndexTitleForSectionName
— by default, section names obtain their values from the value of the attribute that is used for object grouping. By implementing this method we can change the name, altering the default value (sectionName
argument) or writing in an entirely different value. The returned string is the new value. We’ll just return the default value.controllerWillChangeContent
— the method notifies the delegate that the controller is about to start processing one or more changes by request that our controller works with. In it, we’ll invoke theUITableView
table method —beginUpdates
.didChangeSection
— the delegate tracks the method for data changes in the model that lead to changes in sections. The method accepts the following arguments:sectionInfo
— describes a section that was changed,sectionIndex
— the section’s index and typeNSFetchedResultsChangeType
that describes the change type (Insert, Delete, Move, Update). In this method we’ll describe animated section addition and deletion.didChangeObject
— this method is similar to the previous one, but instead of the sectionInfo argument that describes the section, there’s theanObject
argument that is a modifiable object, and instead of sectionIndex there is the old index that the object had before the changes —indexPath
, andnewIndexPath
that it obtained after the changes. UsingUITableView**
methods, we’ll handle animated object addition, deletion, movement and updating.controllerDidChangeContent
— the method notifies the delegate that the changes have taken place. We’ll call theendUpdates
table method in it.
For display purposes, let’s add two new objects: “Banana"
and "Cheddar Cheese"
, both with a price of 1.
The result
The Cache
As for caching, the controller can cache the objects to avoid repeating the same data request tasks. It is rational to use caching for requests that are not changed during the application’s work. If it’s necessary to change the request, we’ll just call the (deleteCacheWithName:)
method to prevent errors when using the same cache for different requests. The requests are cached to the Core Data
file with the name that is assigned in cacheName on controller initialization.
How does cache work?
- If no cache is found by the assigned
cacheName
, the controller calculates the required section and object information and writes it to disk. - If there is a cache, the controller verifies whether it’s valid (checks the entity name, hash version, section key-path and sort descriptors). If the cache is valid, it’s used; otherwise, the controller updates the cache.
Summary
The properties of the NSFetchRequest
class described in this article, together with their in-use examples, demonstrate that the request functionality presented by the Core Data
framework is very flexible and can be used to efficiently fetch data from your app’s database. The Fetched Results Controller
tool allows you to track changes in your model’s objects and easily converts fetched data into UITableView
entries.