Здравствуйте! Эта статья нацелена на разработчиков, у которых есть минимальный навык работы с Core Data Framework
. Напомню, что Core Data
— это фреймворк для хранения данных на устройстве и взаимодействия с ними. На эту тему есть куча русскоязычных статей на хабре и в сети, поэтому не вижу необходимости повторять их содержание.
Зачастую начинающие особенно Stack Overflow разработчики пугаются использовать Core Data Framework
, потому что он кажется им сложным, или используют лишь малую часть его возможностей. В реальности знание базовых функций классов данного фреймворка позволяет разработчику с удобством работать с моделью.
В статье я хочу акцентировать внимание на следующих моментах:
- мы рассмотрим класс
NSFetchRequest
, с помощью которого создаются запросы на извлечение данных из модели. Мы изучим его основные свойства и кейсы с их применением; - мы подробно разберём функции и работу
NSFetchedResultsController
по эффективному представлению извлечённых данных с помощьюNSFetchRequest
на примереUITableView
.
Описание демо-проекта
Демо-проект, на котором мы будем «ставить опыты», весьма примитивен. Он включает Model
и ViewController
, в котором находится UITableView
.
Для конкретики будем использовать банальный список продуктов с наименованием и ценой.
Модель
Модель будет содержать в себе две сущности: Products
с атрибутами name
и price
и FavoriteProducts
, наследующую эти атрибуты.
Для примера заполним нашу базу данных некоторым количеством продуктов с рандомными ценой (до 1000
) и именем продукта из списка: ”Молоко”, “Квас”, “Булочка”, “Банан”, “Колбаса «Молочная»”, “Колбаса «Краковская»”, “Рис”, “Греча”
.
Контроллер
В контроллере с помощью кода инициализируем и размещаем таблицу во весь экран.
- (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;
}
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
— главный герой этой статьи.
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];
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
@property (nonatomic, strong) NSArray <NSSortDescriptor *> *sortDescriptors
var sortDescriptors: [NSSortDescriptor]?
Начать обзор свойств класса хочется с sortDesctriptors
, который представляет собой массив объектов класса NSSortDescriptor
. Именно с помощью них осуществляется механизм сортировки. С инструкциями по использованию дескрипторов сортировки можно познакомиться на портале Apple. Данное свойство принимает массив дескрипторов сортировки, что позволяет нам использовать несколько правил сортировки. Приоритеты при таком использовании равносильны правилам очереди (FIFO, First In-First Out): чем меньше индекс, по которому находится объект в массиве, тем выше приоритет сортировки.
Пример использования
Вывод всех объектов, которые мы рассмотрели ранее, хаотичный и не особо читаемый, Для удобства отсортируем этот список сначала по названию продукта, указав ключом сортировки имя атрибута name
, а потом отсортируем по цене (price
). Имена хотим отсортировать по алфавиту, а цену по возрастанию. Для этого значение ascending
обоих предикатов устанавливаем как булево true
(булево false
используется для сортировки по убыванию).
NSSortDescriptor *nameSortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"name" ascending:YES];
NSSortDescriptor *priceSortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"price" ascending:YES];
fetchRequest.sortDescriptors = @[nameSortDescriptor, priceSortDescriptor];
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
@property (nonatomic, strong) NSPredicate *predicate
var predicate: NSPredicate?
Следующее рассматриваемое свойство — predicate
класса NSPredicate
, которое является мощным и быстрым инструментом для фильтрации данных. По использованию предиката существует отличный гайд от Apple (Перевод). Фильтрация данных при запросе осуществляется благодаря особому строковому синтаксису предиката, который описан в вышеупомянутом гайде.
Пример использования
Начнём с простого примера: мы — страстные любители молочной колбасы и хотим узнать цены на неё, представленные в списке продуктов. Для этого в предикате укажем, что мы хотим получить объекты, у которых имя атрибута name
равно строке "Колбаса «Молочная»"
.
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"name == %@", @"Колбаса «Молочная»"];
fetchRequest.predicate = predicate;
let predicate = NSPredicate(format: "name == %@", "Колбаса «Молочная»")
fetchRequest.predicate = predicate
NAME: Колбаса «Молочная», PRICE: 400
NAME: Колбаса «Молочная», PRICE: 221
Заметьте, что для правильного составления предиката с использованием оператора равенства ==
необходимо точно указывать строковое значение с учётом регистра.
А если мы хотим посмотреть цены не только молочной, а всех видов колбас? Для этого обратимся к оператору CONTAINS
(левое выражение СОДЕРЖИТ
правое) и добавим ключевые слова [cd]
, которые указывают на нечувствительность к регистру и диакритическим символам. Также мы можем использовать несколько условий, в чём нам поможет оператор AND
. Ограничим результаты по стоимости — до 500 денежных единиц.
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"name CONTAINS[cd] %@ AND price < %d", @"колбаса", 500];
fetchRequest.predicate = predicate;
let predicate = NSPredicate(format: "name CONTAINS[cd] %@ AND price < %d", "колбаса", 500)
fetchRequest.predicate = predicate
NAME: Колбаса «Молочная», PRICE: 400
NAME: Колбаса «Молочная», PRICE: 221
NAME: Колбаса «Краковская», PRICE: 345
fetchLimit
@property (nonatomic) NSUInteger fetchLimit
var fetchLimit: Int
Свойство fetchLimit
позволяет ограничивать количество извлекаемых объектов.
Пример использования
Для демонстрации получим 12 самых дешёвых товаров из списка продуктов. Для этого добавим сортировку по цене и ограничение на количество извлекаемых объектов — 12
.
// сортировка по цене
NSSortDescriptor *priceSortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"price" ascending:YES];
fetchRequest.sortDescriptors = @[priceSortDescriptor];
// ограничение по количеству = 12
fetchRequest.fetchLimit = 12;
// сортировка по цене
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
@property (nonatomic) NSUInteger fetchOffset
var fetchOffset: Int
С помощью данного свойства можно сместить результаты выборки на заданное количество объектов.
Пример использования
Чтобы показать работу данного свойства, воспользуемся предыдущим запросом и добавим к нему смещение на два объекта. В результате мы получим 12 объектов, где первые два пропущены, а в конец добавлены следующие, как будто мы сместили таблицу с результатами на две ячейки.
fetchRequest.fetchOffset = 2;
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
@property (nonatomic) NSUInteger fetchBatchSize
var fetchBatchSize: Int
С помощью fetchBatchSize
регулируется, сколько объектов за раз будет извлечено из базы данных (Persistent Store
), с которой работает Core Data Framework
(SQLite
, XML
и др.). Правильно установленное значение для конкретных кейсов может как ускорить работу с базой, так и, наоборот, замедлить.
Допустим, мы работаем с UITableView
. В нашей модели более 10000 объектов. Потребуется некоторое время, чтобы извлечь все эти элементы за раз. Но таблица у нас вмещает 20 ячеек на экран, и для отображения нам потребуются только 20 объектов. В таком кейсе целесообразно использовать fetchBatchSize
равное 20. Сначала Core Data
запросит из базы данных 20 объектов, которые мы отобразим в таблице, а при скролле будет запрошена следующая пачка из 20 элементов. Такой подход значительно оптимизирует взаимодействие с постоянным хранилищем.
Но не стоит использовать слишком маленький размер пачки, например, равное 1 — это только нагрузит базу постоянными запросами по одному элементу.
fetchRequest.fetchBatchSize = 20;
fetchRequest.fetchBatchZize = 20
resultType
@property (nonatomic) NSFetchRequestResultType resultType
var resultType: NSFetchRequestResultType
При извлечении данных, метод executeFetchRequest(_:)
по умолчанию возвращает массив объектов класса NSManagedObject
и его наследников.
Изменение свойства resultType
позволяет выбрать тип извлечённых объектов. Рассмотрим их (Objective-C с префиксом NS чередуется со Swift через слэш):
NSManagedObjectResultType
/ManagedObjectResultType
— объекты классаNSManagedObject
и его наследников (по умолчанию).NSManagedObjectIDResultType
/ManagedObjectIDResultType
— идентификатор объектаNSManagedObject
.NSDictionaryResultType
/DictionaryResultType
— словарь, где ключи — это имена атрибутов сущности.NSCountResultType
/CountResultType
— вернёт один элемент массива со значением количества элементов.
propertiesToFetch
@property (nonatomic, copy) NSArray *propertiesToFetch
var propertiesToFetch: [AnyObject]?
Данное свойство позволяет извлекать из сущности только необходимые атрибуты. Но обязательное условием является, что resultType
должен быть словарём (NSDictionaryResultType
/ DictionaryResultType
).
Пример использования
В качестве примера извлечём только значения атрибута name
и для вывода распечатаем все значения по всем существующим ключам словаря в формате (key: value)
.
fetchRequest.resultType = NSDictionaryResultType;
fetchRequest.propertiesToFetch = @[@"name"];
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
@property (nonatomic) BOOL includesSubentities
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
.
fetchRequest.includesSubentities = NO;
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
- (instancetype)initWithFetchRequest:(NSFetchRequest *)fetchRequest managedObjectContext: (NSManagedObjectContext *)context sectionNameKeyPath:(nullable NSString *)sectionNameKeyPath cacheName:(nullable NSString *)name;
public init(fetchRequest: NSFetchRequest, managedObjectContext context: NSManagedObjectContext, sectionNameKeyPath: String?, cacheName name: String?)
Разберём параметры инициализации:
fetchRequest
— запрос на извлечение объектовNSFetchRequest
. Важно: для работыFRC
необходимо, чтобы у запроса был хотя бы один дескриптор сортировки и егоresultType
должен бытьNSManagedObjectResultType / ManagedObjectResultType
.context
— контекстNSManagedObjectContext
в котором мы работаем.sectionNameKeyPath
— необязательный параметр, при указании которого в формате строкового ключа (имени атрибута сущности) происходит группировка объектов с одинаковыми значениями этого атрибута в секции таблицы. Важно, чтобы этот ключ совпадал с дескриптором сортировки, у которого самый высокий приоритет. Если не указывать этот параметр, будет создана таблица с одной секцией.cacheName
— необязательный параметр, при указании которого контроллер начинает кэшировать результаты запросов. Рассмотрим его позже более детально.
Следующим шагом мы должны вызвать метод контроллера performFetch
для того, чтобы сделать извлечение выборки из базы данных.
NSError *error = nil;
if (![self.fetchedResultsController performFetch:&error]) {
NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
abort();
}
do {
try fetchedResultsController.performFetch()
} catch {
print(error)
}
Метод возвращает булево значение. Если извлечение произведено успешно, то вернётся булево true
, а в противном случае — false
. После извлечения объекты находятся в свойстве контроллера fetchedObjects
.
Взаимодейстие с UITableView
Рассмотрим работу с таблицей. Несмотря на то, что извлеченные объекты находятся в свойстве fetchedObject
, для работы с ними следует обращаться к свойству контроллера sections
. Это массив объектов, которые подписаны на протокол NSFetchedResultsSectionInfo
, в котором описаны следующие свойства:
name
— имя секции.indexTitle
— заголовок секции.numbersOfObjects
— колличество объектов в секции.objects
— сам массив объектов, находящихся в секции.
Реализация
Чтобы нам было удобно, добавим метод для конфигурации ячейки таблицы configureCell
.
#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;
}
// 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"
, получим таблицу с группировкой наших продуктов по названию.
Режимы работы FRC
FRC может работать в нескольких режимах работы:
- У контроллера нет делегата и для него не указано имя кэша
(delegate = nil, cacheName = nil)
— в этом режиме данные берутся только при извлечении запроса и не кэшируются. - Контроллеру присвоен делегат, но имени кэша по прежнему не указано
(delegate != nil, cacheName = nil)
— режим мониторинга изменения с использованием методов протоколаNSFetchedResultsControllerDelegate
, описание которому будет чуть позже. Кэширования объектов не происходит. - Контроллеру присвоены и делегат, и имя кэша
(delegate != nil, cacheName = <#NSString/String#>)
— режим мониторинга с кэшированием объектов.
NSFetchedResultsControllerDelegate
NSFetchedResultsControllerDelegate
предоставляет механизмы, с помощью которых можно отлавливать изменения, происходящие с объектами нашего NSFetchRequest
запроса в модели. На примере с UITableView
рассмотрим, как без ущерба для UI-представления отобразить изменения, произошедшие в модели.
#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];
}
// 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()
}
}
Разберем методы делегирования и их применение
-
sectionIndexTitleForSectionName
— по умолчанию названия секций получают своё значение из значения атрибута, по которому происходит группировка объектов. Имплементируя этот метод, мы можем изменить название, преобразовав значение по умолчанию (аргументsectionName
) или написав любое другое. Возвращаемая строка является новым значением. Мы просто вернём значение по умолчанию. -
controllerWillChangeContent
— метод оповещает делегат о начале изменения объектов по запросу, с которым работает наш контроллер. Вызовем в нём методUITableView
таблицы —beginUpdates
. -
didChangeSection
— метод отлавливается делегатом, когда происходит обновление данных в модели, повлиявших на изменения в секциях. Метод принимает аргументы:sectionInfo
— описывающий секцию, с которой происходит изменение,sectionIndex
— индекс этой секции иtype
NSFetchedResultsChangeType
, который описывает тип изменения (Insert, Delete, Move, Update). В этом методе опишем добавление и удаление секции с использованием анимации. -
didChangeObject
— данный метод работает по аналогии с предыдущим, только вместо аргумента, описывающего секциюsectionInfo
, приходит аргументanObject
, который является модифицируемым объектом, а также вместоsectionIndex
— старый индекс, который был у объекта до изменений `indexPath и новыйnewIndexPath
, который он получил после изменений. Используя методыUITableView
**, обработаем добавление, удаление, перемещение, и обновление объектов с использованием анимаций. controllerDidChangeContent
— метод оповещает делегат о конце изменений. В нём вызываем метод таблицыendUpdates
.
Для демонстрации работы добавим два новых объекта — "Банан"
и "Апельсин"
цена которых 1
.
The Cache
Поговорим о кэшировании. Контроллер умеет кэшировать объекты в целях избежания повторения одних и тех же задач по запросу данных. Кэширование целесообразно использовать для неизменяемых в процессе работы приложения запросов. При необходимости изменить запрос мы вызываем метод (deleteCacheWithName:)
, чтобы избежать ошибок при использовании одного и того же кэша для разных запросов. Запросы кэшируются в файл Core Data с именем, назначаемым в cacheName
при инициализации контроллера.
Как же работает кэш?
- Если по заданному имени
cacheName
кэш не найден, то контроллер высчитывает необходимую информацию по секциям и объектам в них и производит запись на диск. - Если же кэш найден, контроллер проверяет его актуальность (проверяет сущность, версию хэша, ключа секций и дескрипторов сортировки). Если кэш актуален — он его использует, если нет, то обновляет.
Резюме
Описанные в этой статье свойства класса NSFetchRequest
и примеры их использования показывают, что представленный фреймворком Core Data
функционал запросов весьма гибок, и с помощью него можно эффективно извлекать данные из базы данных вашего приложения. Инструмент Fetched Results Controller
позволит отслеживать изменения, произошедшие с объектами модели, а также удобно преобразует извлеченные данные в элементы UITableView
.
В комментариях приветствуются ваши вопросы, замечания или примеры из личного опыта, дополняющие статью.
Автор: Лайв Тайпинг