Работа NSFetchRequest и NSFetchedResultsController, а также зачем тут продуктовый рынок

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


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


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


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

    image

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


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


    Модель


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



    Для примера заполним нашу базу данных некоторым количеством продуктов с рандомными ценой (до 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 (SQLite, XML и др.). Правильно установленное значение для конкретных кейсов может как ускорить работу с базой, так и, наоборот, замедлить.


    Допустим, мы работаем с 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", получим таблицу с группировкой наших продуктов по названию.


    Результат


    Режимы работы 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 — индекс этой секции и type NSFetchedResultsChangeType, который описывает тип изменения (Insert, Delete, Move, Update). В этом методе опишем добавление и удаление секции с использованием анимации.


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


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

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


    Результат


    The Cache


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


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


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

    Резюме


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


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

    Лайв Тайпинг
    Мы создаём мобильные приложения и веб-сервисы
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 3

    • UFO just landed and posted this here
        0

        Статья очень хорошая и полезная, спасибо автору за его труд.
        Но так же было бы очень приятно видеть не только описание FRC, но и заметку о его проблемах и в каких случаях его лучше не использовать. Тогда бы статья была из ряда must have по core data.


        P.S. Почему в методе конфигурации ячейки вы делаете
        return cell ?? UITableViewCell()
        если строкой выше вы уже используете force-unwrap :)

          0
          Спасибо за замечание по поводу опционала UITableViewCell, исправил)

        Only users with full accounts can post comments. Log in, please.