Работа NSFetchRequest и NSFetchedResultsController в Core Data Framework

Эта статья нацелена на разработчиков, у которых есть минимальный навык работы с Core Data Framework. Напомню, что Core Data — это фреймворк для хранения данных на устройстве и взаимодействия с ними. На эту тему есть куча русскоязычных статей на хабре и в сети, поэтому не вижу необходимости повторять их содержание.

Зачастую начинающие разработчики пугаются использовать Core Data Framework, потому что он кажется им сложным, или используют лишь малую часть его возможностей. В реальности знание базовых функций классов данного фреймворка позволяет разработчику с удобством работать с моделью.


В статье я хочу акцентировать внимание на следующих моментах:

  • мы рассмотрим класс NSFetchRequest, с помощью которого создаются запросы на извлечение данных из модели. Мы изучим его основные свойства и кейсы с их применением;
  • мы подробно разберём функции и работу NSFetchedResultsController по эффективному представлению извлечённых данных с помощью NSFetchRequest на примере UITableView.

Описание демо-проекта

Демо-проект, на котором мы будем «ставить опыты», весьма примитивен. Он включает Model и ViewController, в котором находится UITableView. Для конкретики будем использовать банальный список продуктов с наименованием и ценой.

Модель

Модель будет содержать в себе две сущности: Products с атрибутами name и price и FavoriteProducts, наследующую эти атрибуты.


0a3b26d2fcb0482aaf170e5d0d12f39a.png


Для примера заполним нашу базу данных некоторым количеством продуктов с рандомными ценой (до 1000) и именем продукта из списка: «Молоко», «Квас», «Булочка», «Банан», «Колбаса "Молочная"», «Колбаса "Краковская"», «Рис», «Греча».

Контроллер

В контроллере с помощью кода инициализируем и размещаем таблицу во весь экран.


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
}

Извлечение данных

Извлечение данных из модели осуществляется методом NSManagedObjectContext executeFetchRequest(_:). Аргументом метода является запрос выборки NSFetchRequest — главный герой этой статьи. 


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)")
}

Возвращаемым типом метода executeFetchRequest(_:) является массив объектов класса NSManagedObjectпо умолчанию. Для наглядности распечатаем извлечённые из нашей модели элементы, преобразовав вывод.


Вывод

NAME: Булочка, PRICE: 156
NAME: Квас, PRICE: 425
NAME: Квас, PRICE: 85
NAME: Колбаса «Молочная», PRICE: 400
NAME: Рис, PRICE: 920
NAME: Колбаса «Краковская», PRICE: 861
NAME: Квас, PRICE: 76
NAME: Молоко, PRICE: 633
NAME: Квас, PRICE: 635
NAME: Колбаса «Краковская», PRICE: 718
NAME: Булочка, PRICE: 701
NAME: Квас, PRICE: 176
NAME: Банан, PRICE: 731
NAME: Колбаса «Краковская», PRICE: 746
NAME: Рис, PRICE: 456
NAME: Рис, PRICE: 519
NAME: Колбаса «Молочная», PRICE: 221
NAME: Рис, PRICE: 560
NAME: Колбаса «Краковская», PRICE: 646
NAME: Булочка, PRICE: 492
NAME: Банан, PRICE: 185
NAME: Квас, PRICE: 539
NAME: Колбаса «Краковская», PRICE: 872
NAME: Банан, PRICE: 972
NAME: Булочка, PRICE: 821
NAME: Молоко, PRICE: 409
NAME: Банан, PRICE: 334
NAME: Молоко, PRICE: 734
NAME: Квас, PRICE: 448
NAME: Колбаса «Краковская», PRICE: 345

Основные методы и свойства класса NSFetchRequest

Как я уже сказал выше, класс NSFetchRequest используется в качестве запроса выборки данных из модели. Этот инструмент позволяет задавать правила фильтрации и сортировки объектов на этапе извлечения их из базы данных. Данная операция становится во много раз эффективнее и производительнее, чем если бы мы сначала делали извлечение всех объектов (а если их 10000 и больше?), а потом вручную сортировали или производили фильтрацию интересующих нас данных.

Зная основные свойства этого класса, можно с лёгкостью оперировать запросами и получать конкретную выборку без разработки дополнительных алгоритмов и костылей — всё уже реализовано в Core Data. Приступим.


sortDescriptors

Objective-C

@property (nonatomic, strong) NSArray <NSSortDescriptor *> *sortDescriptors

Swift

var sortDescriptors: [NSSortDescriptor]?

Начать обзор свойств класса хочется с sortDesctriptors, который представляет собой массив объектов класса NSSortDescriptor. Именно с помощью них осуществляется механизм сортировки. С инструкциями по использованию дескрипторов сортировки можно познакомиться на портале Apple. Данное свойство принимает массив дескрипторов сортировки, что позволяет нам использовать несколько правил сортировки. Приоритеты при таком использовании равносильны правилам очереди (FIFO, First In-First Out): чем меньше индекс, по которому находится объект в массиве, тем выше приоритет сортировки.


Пример использования

Вывод всех объектов, которые мы рассмотрели ранее, хаотичный и не особо читаемый, Для удобства отсортируем этот список сначала по названию продукта, указав ключом сортировки имя атрибута name, а потом отсортируем по цене (price). Имена хотим отсортировать по алфавиту, а цену по возрастанию. Для этого значение ascending обоих предикатов устанавливаем как булево true (булево false используется для сортировки по убыванию).



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]

Результат работы сортировки

NAME: Банан, PRICE: 185
NAME: Банан, PRICE: 334
NAME: Банан, PRICE: 731
NAME: Банан, PRICE: 972
NAME: Булочка, PRICE: 156
NAME: Булочка, PRICE: 492
NAME: Булочка, PRICE: 701
NAME: Булочка, PRICE: 821
NAME: Квас, PRICE: 76
NAME: Квас, PRICE: 85
NAME: Квас, PRICE: 176
NAME: Квас, PRICE: 425
NAME: Квас, PRICE: 448
NAME: Квас, PRICE: 539
NAME: Квас, PRICE: 635
NAME: Колбаса «Краковская», PRICE: 345
NAME: Колбаса «Краковская», PRICE: 646
NAME: Колбаса «Краковская», PRICE: 718
NAME: Колбаса «Краковская», PRICE: 746
NAME: Колбаса «Краковская», PRICE: 861
NAME: Колбаса «Краковская», PRICE: 872
NAME: Колбаса «Молочная», PRICE: 221
NAME: Колбаса «Молочная», PRICE: 400
NAME: Молоко, PRICE: 409
NAME: Молоко, PRICE: 633
NAME: Молоко, PRICE: 734
NAME: Рис, PRICE: 456
NAME: Рис, PRICE: 519
NAME: Рис, PRICE: 560
NAME: Рис, PRICE: 920

predicate


Objective-C


@property (nonatomic, strong) NSPredicate *predicate

Swift


var predicate: NSPredicate?

Следующее рассматриваемое свойство — predicate класса NSPredicate, которое является мощным и быстрым инструментом для фильтрации данных. По использованию предиката существует отличный гайд от Apple (перевод). Фильтрация данных при запросе осуществляется благодаря особому строковому синтаксису предиката, который описан в вышеупомянутом гайде.


Пример использования

Начнём с простого примера: мы — страстные любители молочной колбасы и хотим узнать цены на неё, представленные в списке продуктов. Для этого в предикате укажем, что мы хотим получить объекты, у которых имя атрибута name равно строке "Колбаса «Молочная»".


Objective-C

 NSPredicate *predicate = [NSPredicate predicateWithFormat:@"name == %@", @"Колбаса «Молочная»"];
 fetchRequest.predicate = predicate;

Swift

let predicate = NSPredicate(format: "name == %@", "Колбаса «Молочная»")
fetchRequest.predicate = predicate

Результат

NAME: Колбаса «Молочная», PRICE: 400
NAME: Колбаса «Молочная», PRICE: 221

Заметьте, что для правильного составления предиката с использованием оператора равенства ==необходимо точно указывать строковое значение с учётом регистра.

А если мы хотим посмотреть цены не только молочной, а всех видов колбас? Для этого обратимся к оператору CONTAINS (левое выражение СОДЕРЖИТ правое) и добавим ключевые слова [cd], которые указывают на нечувствительность к регистру и диакритическим символам. Также мы можем использовать несколько условий, в чём нам поможет оператор AND. Ограничим результаты по стоимости — до 500 денежных единиц.


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

Результат

NAME: Колбаса «Молочная», PRICE: 400
NAME: Колбаса «Молочная», PRICE: 221
NAME: Колбаса «Краковская», PRICE: 345

fetchLimit


Objective-C

@property (nonatomic) NSUInteger fetchLimit

Swift

var fetchLimit: Int

Свойство fetchLimit позволяет ограничивать количество извлекаемых объектов.


Пример использования

Для демонстрации получим 12 самых дешёвых товаров из списка продуктов. Для этого добавим сортировку по цене и ограничение на количество извлекаемых объектов — 12.

Objective-C

// сортировка по цене
NSSortDescriptor *priceSortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"price" ascending:YES];
fetchRequest.sortDescriptors = @[priceSortDescriptor];
// ограничение по количеству = 12
fetchRequest.fetchLimit = 12;

Swift

// сортировка по цене
let priceSortDescriptor = NSSortDescriptor(key: "price", ascending: true)
fetchRequest.sortDescriptors = [priceSortDescriptor]
// ограничение по количеству = 12
fetchRequest.fetchLimit = 12

Результат

NAME: Квас, PRICE: 76
NAME: Квас, PRICE: 85
NAME: Булочка, PRICE: 156
NAME: Квас, PRICE: 176
NAME: Банан, PRICE: 185
NAME: Колбаса «Молочная», PRICE: 221
NAME: Банан, PRICE: 334
NAME: Колбаса «Краковская», PRICE: 345
NAME: Колбаса «Молочная», PRICE: 400
NAME: Молоко, PRICE: 409
NAME: Квас, PRICE: 425
NAME: Квас, PRICE: 448

fetchOffset


Objective-C

@property (nonatomic) NSUInteger fetchOffset

Swift

var fetchOffset: Int

С помощью данного свойства можно сместить результаты выборки на заданное количество объектов.


Пример использования

Чтобы показать работу данного свойства, воспользуемся предыдущим запросом и добавим к нему смещение на два объекта. В результате мы получим 12 объектов, где первые два пропущены, а в конец добавлены следующие, как будто мы сместили таблицу с результатами на две ячейки.

Objective-C

fetchRequest.fetchOffset = 2;

Swift

fetchRequest.fetchOffset = 2


Для наглядности я ограничил извлечённые объекты пунктиром.


Результат

Было:                                       Стало:
                                            NAME: Квас, PRICE: 76
                                            NAME: Квас, PRICE: 85
---------------------------------------------------------------------------------
NAME: Квас, PRICE: 76                       NAME: Булочка, PRICE: 156
NAME: Квас, PRICE: 85                       NAME: Квас, PRICE: 176
NAME: Булочка, PRICE: 156                   NAME: Банан, PRICE: 185
NAME: Квас, PRICE: 176                      NAME: Колбаса «Молочная», PRICE: 221
NAME: Банан, PRICE: 185                     NAME: Банан, PRICE: 334
NAME: Колбаса «Молочная», PRICE: 221        NAME: Колбаса «Краковская», PRICE: 345
NAME: Банан, PRICE: 334                     NAME: Колбаса «Молочная», PRICE: 400
NAME: Колбаса «Краковская», PRICE: 345      NAME: Молоко, PRICE: 409
NAME: Колбаса «Молочная», PRICE: 400        NAME: Квас, PRICE: 425
NAME: Молоко, PRICE: 409                    NAME: Квас, PRICE: 448
NAME: Квас, PRICE: 425                      NAME: Рис, PRICE: 456
NAME: Квас, PRICE: 448                      NAME: Булочка, PRICE: 492
---------------------------------------------------------------------------------
NAME: Рис, PRICE: 456                       ...
NAME: Булочка, PRICE: 492
...

fetchBatchSize


Objective-C

@property (nonatomic) NSUInteger fetchBatchSize

Swift

var fetchBatchSize: Int

С помощью fetchBatchSize регулируется, сколько объектов за раз будет извлечено из базы данных (Persistent Store), с которой работает Core Data Framework (SQLiteXML и др.). Правильно установленное значение для конкретных кейсов может как ускорить работу с базой, так и, наоборот, замедлить.

Допустим, мы работаем с UITableView. В нашей модели более 10000 объектов. Потребуется некоторое время, чтобы извлечь все эти элементы за раз. Но таблица у нас вмещает 20 ячеек на экран, и для отображения нам потребуются только 20 объектов. В таком кейсе целесообразно использовать fetchBatchSize равное 20. Сначала Core Data запросит из базы данных 20 объектов, которые мы отобразим в таблице, а при скролле будет запрошена следующая пачка из 20 элементов. Такой подход значительно оптимизирует взаимодействие с постоянным хранилищем.

Но не стоит использовать слишком маленький размер пачки, например, равное 1 — это только нагрузит базу постоянными запросами по одному элементу.


Objective-C

fetchRequest.fetchBatchSize = 20;


Swift

fetchRequest.fetchBatchZize = 20

resultType


Objective-C

@property (nonatomic) NSFetchRequestResultType resultType

Swift

var resultType: NSFetchRequestResultType

При извлечении данных, метод executeFetchRequest(_:) по умолчанию возвращает массив объектов класса NSManagedObject и его наследников.

Изменение свойства resultType позволяет выбрать тип извлечённых объектов. Рассмотрим их (Objective-C с префиксом NS чередуется со Swift через слэш):


  • NSManagedObjectResultType / ManagedObjectResultType — объекты класса NSManagedObject и его наследников (по умолчанию).
  • NSManagedObjectIDResultType / ManagedObjectIDResultType — идентификатор объекта NSManagedObject.
  • NSDictionaryResultType / DictionaryResultType — словарь, где ключи — это имена атрибутов сущности.
  • NSCountResultType / CountResultType — вернёт один элемент массива со значением количества элементов.

propertiesToFetch


Objective-C

@property (nonatomic, copy) NSArray *propertiesToFetch

Swift

var propertiesToFetch: [AnyObject]?

Данное свойство позволяет извлекать из сущности только необходимые атрибуты. Но обязательное условием является, что resultType должен быть словарём (NSDictionaryResultType / DictionaryResultType).


Пример использования


В качестве примера извлечём только значения атрибута name и для вывода распечатаем все значения по всем существующим ключам словаря в формате (key: value).

Objective-C

fetchRequest.resultType = NSDictionaryResultType;
fetchRequest.propertiesToFetch = @[@"name"];

Swift

fetchRequest.resultType = .DictionaryResultType
fetchRequest.propertiesToFetch = ["name"]

Результат (ключ: значение)

name: Булочка
name: Квас
name: Квас
name: Колбаса «Молочная»
name: Рис
name: Колбаса «Краковская»
name: Квас
name: Молоко
name: Квас
name: Колбаса «Краковская»
name: Булочка
name: Квас
name: Банан
name: Колбаса «Краковская»
name: Рис
name: Рис
name: Колбаса Молочная
name: Рис
name: Колбаса «Краковская»
name: Булочка
name: Банан
name: Квас
name: Колбаса «Краковская»
name: Банан
name: Булочка
name: Молоко
name: Банан
name: Молоко
name: Квас
name: Колбаса «Краковская»

includesSubentities


Objective-C

@property (nonatomic) BOOL includesSubentities

Swift

var includesSubentities: Bool

Для демонстрации этого свойства необходимо добавить объект в сущность FavoriteProducts, которая является наследником Products. Присвоим этому объекту имя "ЯБЛОКО" и цену 999.

Обратимся к запросу сущности Products, с помощью которого мы выполняли сортировку по имени и цене.


Результат

NAME: Банан, PRICE: 185
NAME: Банан, PRICE: 334
NAME: Банан, PRICE: 731
NAME: Банан, PRICE: 972
NAME: Булочка, PRICE: 156
NAME: Булочка, PRICE: 492
NAME: Булочка, PRICE: 701
NAME: Булочка, PRICE: 821
NAME: Квас, PRICE: 76
NAME: Квас, PRICE: 85
NAME: Квас, PRICE: 176
NAME: Квас, PRICE: 425
NAME: Квас, PRICE: 448
NAME: Квас, PRICE: 539
NAME: Квас, PRICE: 635
NAME: Колбаса «Краковская», PRICE: 345
NAME: Колбаса «Краковская», PRICE: 646
NAME: Колбаса «Краковская», PRICE: 718
NAME: Колбаса «Краковская», PRICE: 746
NAME: Колбаса «Краковская», PRICE: 861
NAME: Колбаса «Краковская», PRICE: 872
NAME: Колбаса «Молочная», PRICE: 221
NAME: Колбаса «Молочная», PRICE: 400
NAME: Молоко, PRICE: 409
NAME: Молоко, PRICE: 633
NAME: Молоко, PRICE: 734
NAME: Рис, PRICE: 456
NAME: Рис, PRICE: 519
NAME: Рис, PRICE: 560
NAME: Рис, PRICE: 920
NAME: ЯБЛОКО, PRICE: 999

В конце списка заметим объект, который мы только что добавили для сущности FavoriteProducts. Что же он тут делает? Дело в том, что у запроса значение свойства includesSubentities по умолчанию равно булевому true, что означает извлечение объектов не только текущей сущности, но и сущностей-наследников.

Чтобы избежать этого, изменим его на булево false.


Objective-C

fetchRequest.includesSubentities = NO;

Swift

fetchRequest.includesSubentities = false


Результат

NAME: Банан, PRICE: 185
NAME: Банан, PRICE: 334
NAME: Банан, PRICE: 731
NAME: Банан, PRICE: 972
NAME: Булочка, PRICE: 156
NAME: Булочка, PRICE: 492
NAME: Булочка, PRICE: 701
NAME: Булочка, PRICE: 821
NAME: Квас, PRICE: 76
NAME: Квас, PRICE: 85
NAME: Квас, PRICE: 176
NAME: Квас, PRICE: 425
NAME: Квас, PRICE: 448
NAME: Квас, PRICE: 539
NAME: Квас, PRICE: 635
NAME: Колбаса «Краковская», PRICE: 345
NAME: Колбаса «Краковская», PRICE: 646
NAME: Колбаса «Краковская», PRICE: 718
NAME: Колбаса «Краковская», PRICE: 746
NAME: Колбаса «Краковская», PRICE: 861
NAME: Колбаса «Краковская», PRICE: 872
NAME: Колбаса «Молочная», PRICE: 221
NAME: Колбаса «Молочная», PRICE: 400
NAME: Молоко, PRICE: 409
NAME: Молоко, PRICE: 633
NAME: Молоко, PRICE: 734
NAME: Рис, PRICE: 456
NAME: Рис, PRICE: 519
NAME: Рис, PRICE: 560
NAME: Рис, PRICE: 920

Fetched Results Controller (FRC)

Контроллер класса NSFetchedResultsController условно можно расположить между Core Data и ViewController, в котором нам нужно отобразить данные из базы. Методы и свойства этого контроллера позволяют с удобством взаимодействовать, представлять и управлять объектами из Core Data в связке с таблицами UITableView, для которых он наиболее адаптирован.

Этот контроллер умеет преобразовывать извлечённые объекты в элементы таблицы — секции и объекты этих секций. FRC имеет протокол NSFetchedResultsControllerDelegate, который при делегировании позволяет отлавливать изменения происходящих с объектами заданного запроса NSFetchRequest в момент инициализации контроллера.


Инициализация FRC



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?)

Разберём параметры инициализации:


  • fetchRequest — запрос на извлечение объектов NSFetchRequest. Важно: для работы FRC необходимо, чтобы у запроса был хотя бы один дескриптор сортировки и его resultType должен быть NSManagedObjectResultType / ManagedObjectResultType.
  • context — контекст NSManagedObjectContext в котором мы работаем.
  • sectionNameKeyPath — необязательный параметр, при указании которого в формате строкового ключа (имени атрибута сущности) происходит группировка объектов с одинаковыми значениями этого атрибута в секции таблицы. Важно, чтобы этот ключ совпадал с дескриптором сортировки, у которого самый высокий приоритет. Если не указывать этот параметр, будет создана таблица с одной секцией.
  • cacheName — необязательный параметр, при указании которого контроллер начинает кэшировать результаты запросов. Рассмотрим его позже более детально.

Следующим шагом мы должны вызвать метод контроллера performFetch для того, чтобы сделать извлечение выборки из базы данных.


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)
}

Метод возвращает булево значение. Если извлечение произведено успешно, то вернётся булево true, а в противном случае — false. После извлечения объекты находятся в свойстве контроллера fetchedObjects.


Взаимодейстие с UITableView


Рассмотрим работу с таблицей. Несмотря на то, что извлеченные объекты находятся в свойстве fetchedObject, для работы с ними следует обращаться к свойству контроллера sections. Это массив объектов, которые подписаны на протокол NSFetchedResultsSectionInfo, в котором описаны следующие свойства:


  • name — имя секции.
  • indexTitle — заголовок секции.
  • numbersOfObjects — количество объектов в секции.
  • objects — сам массив объектов, находящихся в секции.

Реализация

Чтобы нам было удобно, добавим метод для конфигурации ячейки таблицы configureCell.


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!
    }
}

Используя запрос NSFetchRequest с сортировкой по названию и цене и указав sectionNameKeyPath для FRCкак имя атрибута "name", получим таблицу с группировкой наших продуктов по названию.


Результат

a8adf12e2a5e461a998ffb1ec7bc9dbb.gif


Режимы работы FRC


FRC может работать в нескольких режимах работы:


  • У контроллера нет делегата и для него не указано имя кэша (delegate = nil, cacheName = nil) — в этом режиме данные берутся только при извлечении запроса и не кэшируются.
  • Контроллеру присвоен делегат, но имени кэша по прежнему не указано (delegate != nil, cacheName = nil) — режим мониторинга изменения с использованием методов протокола NSFetchedResultsControllerDelegate, описание которому будет чуть позже. Кэширования объектов не происходит.
  • Контроллеру присвоены и делегат, и имя кэша (delegate != nil, cacheName = <#NSString/String#>) — режим мониторинга с кэшированием объектов.

NSFetchedResultsControllerDelegate


NSFetchedResultsControllerDelegate предоставляет механизмы, с помощью которых можно отлавливать изменения, происходящие с объектами нашего NSFetchRequest запроса в модели. На примере с UITableView рассмотрим, как без ущерба для UI-представления отобразить изменения, произошедшие в модели.


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()
    }
}

Разберем методы делегирования и их применение:


  1. sectionIndexTitleForSectionName — по умолчанию названия секций получают своё значение из значения атрибута, по которому происходит группировка объектов. Имплементируя этот метод, мы можем изменить название, преобразовав значение по умолчанию (аргумент sectionName) или написав любое другое. Возвращаемая строка является новым значением. Мы просто вернём значение по умолчанию.

  2. controllerWillChangeContent — метод оповещает делегат о начале изменения объектов по запросу, с которым работает наш контроллер. Вызовем в нём метод UITableView таблицы — beginUpdates.

  3. didChangeSection — метод отлавливается делегатом, когда происходит обновление данных в модели, повлиявших на изменения в секциях. Метод принимает аргументы: sectionInfo — описывающий секцию, с которой происходит изменение, sectionIndex — индекс этой секции и typeNSFetchedResultsChangeType, который описывает тип изменения (Insert, Delete, Move, Update). В этом методе опишем добавление и удаление секции с использованием анимации.

  4. didChangeObject — данный метод работает по аналогии с предыдущим, только вместо аргумента, описывающего секцию sectionInfo, приходит аргумент anObject, который является модифицируемым объектом, а также вместо sectionIndex — старый индекс, который был у объекта до изменений `indexPath и новый newIndexPath, который он получил после изменений. Используя методы UITableView**, обработаем добавление, удаление, перемещение, и обновление объектов с использованием анимаций.

  5. controllerDidChangeContent — метод оповещает делегат о конце изменений. В нём вызываем метод таблицы endUpdates.

Для демонстрации работы добавим два новых объекта — "Банан" и "Апельсин" цена которых 1.

Результат:

b4f21cf48587437c8009f30c6ad131ae.gif


The Cache

Поговорим о кэшировании. Контроллер умеет кэшировать объекты в целях избежания повторения одних и тех же задач по запросу данных. Кэширование целесообразно использовать для неизменяемых в процессе работы приложения запросов. При необходимости изменить запрос мы вызываем метод (deleteCacheWithName:), чтобы избежать ошибок при использовании одного и того же кэша для разных запросов. Запросы кэшируются в файл Core Data с именем, назначаемым в cacheName при инициализации контроллера.


Как же работает кэш?


  • Если по заданному имени cacheName кэш не найден, то контроллер высчитывает необходимую информацию по секциям и объектам в них и производит запись на диск.
  • Если же кэш найден, контроллер проверяет его актуальность (проверяет сущность, версию хэша, ключа секций и дескрипторов сортировки). Если кэш актуален — он его использует, если нет, то обновляет.

Резюме

Описанные в этой статье свойства класса NSFetchRequest и примеры их использования показывают, что представленный фреймворком Core Data функционал запросов весьма гибок, и с помощью него можно эффективно извлекать данные из базы данных вашего приложения. Инструмент Fetched Results Controller позволит отслеживать изменения, произошедшие с объектами модели, а также удобно преобразует извлеченные данные в элементы UITableView.

iOS
Development
Core Data Framework