Статьи

Основные данные с нуля: NSFetchedResultsController

В предыдущих выпусках этой серии мы рассмотрели основы инфраструктуры основных данных. Пришло время использовать наши знания, создав простое приложение на базе Core Data.

В этом уроке мы также познакомимся с другим звездным игроком платформы Core Data, классом NSFetchedResultsController . Приложение, которое мы собираемся создать, управляет списком задач. С помощью приложения мы можем добавлять, обновлять и удалять задачи. Вы быстро поймете, что класс NSFetchedResultsController делает это очень легко.

Откройте Xcode, выберите « Создать»> «Проект …» в меню « Файл» и выберите шаблон приложения « Один вид» в разделе « iOS»> « Категория приложения ».

Назовите проект « Готово» , установите « Устройства» на iPhone и скажите Xcode, где вы хотите сохранить проект.

Поскольку мы выбрали шаблон приложения Single View , XCode не создал для нас настройку Core Data. Однако настроить стек основных данных должно быть легко, если вы читали предыдущие части этой серии.

Откройте файл реализации класса делегата приложения, TSPAppDelegate.m , и объявите три свойства в расширении частного класса, managedObjectContext , managedObjectModel и persistentStoreCoordinator . Если вас смущает этот шаг, то я рекомендую вам вернуться к первой статье этой серии , в которой подробно рассматривается стек основных данных.

Обратите внимание, что я также добавил оператор импорта для платформы Core Data в верхней части TSPAppDelegate.m .

01
02
03
04
05
06
07
08
09
10
11
#import «TSPAppDelegate.h»
 
#import <CoreData/CoreData.h>
 
@interface TSPAppDelegate ()
 
@property (strong, nonatomic) NSManagedObjectContext *managedObjectContext;
@property (strong, nonatomic) NSManagedObjectModel *managedObjectModel;
@property (strong, nonatomic) NSPersistentStoreCoordinator *persistentStoreCoordinator;
 
@end

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
— (NSManagedObjectContext *)managedObjectContext {
    if (_managedObjectContext) {
        return _managedObjectContext;
    }
     
    NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
     
    if (coordinator) {
        _managedObjectContext = [[NSManagedObjectContext alloc] init];
        [_managedObjectContext setPersistentStoreCoordinator:coordinator];
    }
     
    return _managedObjectContext;
}
01
02
03
04
05
06
07
08
09
10
11
— (NSManagedObjectModel *)managedObjectModel {
    if (_managedObjectModel) {
        return _managedObjectModel;
    }
     
    NSURL *modelURL = [[NSBundle mainBundle] URLForResource:@»Done» withExtension:@»momd»];
     
    _managedObjectModel = [[NSManagedObjectModel alloc] initWithContentsOfURL:modelURL];
     
    return _managedObjectModel;
}
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
— (NSPersistentStoreCoordinator *)persistentStoreCoordinator {
    if (_persistentStoreCoordinator) {
        return _persistentStoreCoordinator;
    }
     
    NSURL *applicationDocumentsDirectory = [[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] lastObject];
    NSURL *storeURL = [applicationDocumentsDirectory URLByAppendingPathComponent:@»Done.sqlite»];
     
    NSError *error = nil;
    _persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[self managedObjectModel]];
     
    if (![_persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL options:nil error:&error]) {
        NSLog(@»Unresolved error %@, %@», error, [error userInfo]);
        abort();
    }
     
    return _persistentStoreCoordinator;
}

Есть три вещи, о которых вы должны знать. Во-первых, модель данных, которую мы создадим дальше, будет называться Done.momd . Во-вторых, мы назовем резервное хранилище Done, и это будет база данных SQLite . В-третьих, если резервное хранилище несовместимо с моделью данных, мы вызываем abort , убивая приложение. Как я упоминал ранее в этой серии, хотя во время разработки это нормально, вам никогда не следует вызывать abort в производственной abort . Мы еще вернемся к проблемам миграции и несовместимости позже в этой серии.

Хотя наше приложение не будет зависать, если вы попытаетесь запустить его, стек основных данных не будет настроен должным образом. Причина проста, мы еще не создали модель данных. Давайте позаботимся об этом на следующем шаге.

Выберите « Создать»> «Файл …» в меню « Файл» и выберите « Модель данных» в категории « iOS»> «Основные данные ».

Назовите файл Done , дважды проверьте, что он добавлен в цель Done , и скажите Xcode, где его нужно сохранить.

Модель данных будет очень простой. Создайте новый объект и назовите его TSPItem . Добавьте три атрибута к сущности, name типа String , createdAt с типом Date и done с типом Boolean .

Отметьте атрибуты как обязательные, а не необязательные, и установите значение по умолчанию для атрибута done как NO .

Стек основных данных настроен, а модель данных настроена правильно. Пришло время познакомиться с новым классом инфраструктуры Core Data, классом NSFetchedResultsController .

Эта статья не только о классе NSFetchedResultsController , но и о том, что класс NSFetchedResultsController делает за кулисами. Позвольте мне уточнить, что я имею в виду под этим.

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

Всякий раз, когда запись вставляется , обновляется или удаляется в контексте управляемого объекта, контекст управляемого объекта отправляет уведомление через центр уведомлений. Контекст управляемого объекта может публиковать три типа уведомлений:

  • NSManagedObjectContextObjectsDidChangeNotification : это уведомление публикуется каждый раз, когда запись в контексте управляемого объекта вставляется, обновляется или удаляется.
  • NSManagedObjectContextWillSaveNotification : это уведомление публикуется контекстом управляемого объекта до того, как ожидающие изменения фиксируются в резервном хранилище.
  • NSManagedObjectContextDidSaveNotification : это уведомление публикуется контекстом управляемого объекта сразу после ожидающих изменений в резервном хранилище.

Содержимое этих уведомлений идентично, то есть свойство object уведомления — это экземпляр NSManagedObjectContext который разместил уведомление, а словарь userInfo уведомления содержит записи, которые были вставлены , обновлены и удалены .

Суть в том, что для поддержания результатов запроса на получение обновлений требуется значительное количество стандартного кода. Другими словами, если бы мы создавали наше приложение без использования класса NSFetchedResultsController , нам пришлось бы реализовать механизм, который отслеживал бы изменения в контексте управляемого объекта, и соответственно обновлять пользовательский интерфейс. Давайте посмотрим, как NSFetchedResultsController может помочь нам в этой задаче.

Работать с классом NSFetchedResultsController довольно просто. Экземпляр класса NSFetchedResultsController принимает запрос на выборку и имеет объект делегата. Объект NSFetchedResultsController следит за тем, чтобы он NSFetchedResultsController результаты запроса на выборку, отслеживая контекст управляемого объекта, которым был выполнен запрос на выборку.

Если объект NSFetchedResultsController уведомляется о любых изменениях объектом NSManagedObjectContext запроса выборки, он уведомляет своего делегата. Вы можете быть удивлены, как это отличается от контроллера представления, непосредственно контролирующего объект NSManagedObjectContext . NSFetchedResultsController класса NSFetchedResultsController заключается в том, что он обрабатывает уведомления, которые он получает от объекта NSManagedObjectContext и сообщает делегату только то, что ему нужно знать для обновления пользовательского интерфейса в ответ на эти изменения. Методы протокола NSFetchedResultsControllerDelegate должны прояснить это.

1
2
3
4
5
— (void)controllerWillChangeContent:(NSFetchedResultsController *)controller;
— (void)controllerDidChangeContent:(NSFetchedResultsController *)controller;
 
— (void)controller:(NSFetchedResultsController *)controller didChangeSection:(id<NSFetchedResultsSectionInfo>)sectionInfo atIndex:(NSUInteger)sectionIndex forChangeType:(NSFetchedResultsChangeType)type;
— (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(NSIndexPath *)newIndexPath;

Подписи вышеупомянутых методов делегата раскрывают истинное назначение класса NSFetchedResultsController . На iOS класс NSFetchedResultsController был разработан для управления данными, отображаемыми UITableView или UICollectionView . Он сообщает своему делегату, какие именно записи были изменены, как обновить пользовательский интерфейс и когда это сделать.

Не беспокойтесь, если вы все еще не уверены в цели или преимуществах класса NSFetchedResultsController . Это будет иметь больше смысла после того, как мы реализовали протокол NSFetchedResultsControllerDelegate . Давайте NSFetchedResultsController к нашему приложению и NSFetchedResultsController класс NSFetchedResultsController .

Откройте основную раскадровку проекта Main.storyboard , выберите View Controller Scene и внедрите его в контроллер навигации, выбрав « Embed In»> Navigation Controller в меню « Редактор» .

Перетащите объект UITableView в сцену View Controller. создайте выход в классе TSPViewController и подключите его в раскадровке. Не забудьте TSPViewController класс TSPViewController соответствие с протоколами UITableViewDataSource и UITableViewDelegate .

1
2
3
4
5
6
7
#import <UIKit/UIKit.h>
 
@interface TSPViewController : UIViewController <UITableViewDataSource, UITableViewDelegate>
 
@property (weak, nonatomic) IBOutlet UITableView *tableView;
 
@end

Выберите табличное представление, откройте инспектор соединений и подключите источник данных табличного представления и delegate выходы объекту View Controller . С выбранным табличным представлением откройте инспектор атрибутов и установите количество ячеек прототипа равным 1 .

Прежде чем продолжить, нам нужно создать подкласс UITableViewCell для ячейки прототипа. Создайте новый подкласс Objective C, TSPToDoCell , и установите его суперкласс в UITableViewCell . Создайте два выхода: nameLabel типа UILabel и doneButton типа UIButton .

Вернитесь к раскадровке, выберите ячейку прототипа в табличном представлении и установите класс TSPToDoCell в Identity Inspector . Добавьте объект UILabel и UIButton в представление содержимого ячейки и подключите выходы в Инспекторе соединений . Выбрав ячейку прототипа, откройте инспектор атрибутов и установите для идентификатора ячейки прототипа ToDoCell . Этот идентификатор будет служить идентификатором повторного использования ячейки. Макет ячейки прототипа должен выглядеть примерно так, как показано на скриншоте ниже.

Создайте новый подкласс UIViewController и назовите его TSPAddToDoViewController . textField выходной textField типа UITextField в заголовочном файле контроллера представления и UITextFieldDelegate контроллер представления к протоколу UITextFieldDelegate .

1
2
3
4
5
6
7
#import <UIKit/UIKit.h>
 
@interface TSPAddToDoViewController : UIViewController <UITextFieldDelegate>
 
@property (weak, nonatomic) IBOutlet UITextField *textField;
 
@end

Прежде чем добавить контроллер представления в раскадровку, добавьте следующие два действия в файл реализации контроллера представления.

1
2
3
4
5
6
7
8
9
#pragma mark —
#pragma mark Actions
— (IBAction)cancel:(id)sender {
     
}
 
— (IBAction)save:(id)sender {
     
}

Откройте раскадровку еще раз и добавьте элемент кнопки панели с идентификатором Add справа от панели навигации TSPViewController . Добавьте новый контроллер представления в раскадровку и установите его класс в TSPAddToDoViewController в Identity Inspector . Выбрав контроллер представления, выберите « Встроить»> «Контроллер навигации» в меню « Редактор» .

Новый контроллер представления теперь должен иметь панель навигации. Добавьте два элемента панели кнопок на панель навигации, один слева с идентификатором Отмена и один справа с идентификатором Сохранить . Соедините действие « cancel: с элементом левой панели, а действие « save: — с элементом правой панели.

Добавьте объект UITextField в представление контроллера представления и UITextField его на 20 пунктов ниже панели навигации. Текстовое поле должно оставаться в 20 точках ниже панели навигации. Обратите внимание, что ограничение макета в верхней части ссылается на верхнее руководство по макету , очень удобное дополнение, которое было добавлено в iOS 7.

Соедините текстовое поле с соответствующим выходом в контроллере представления и установите контроллер представления в качестве делегата текстового поля. Наконец, перетащите элемент управления из элемента кнопки панели TSPViewController на контроллер навигации, для которого TSPAddToDoViewController является корневым контроллером представления. Установите тип addToDoViewController модальным и установите идентификатор addToDoViewController для addToDoViewController в Инспекторе Атрибутов . Это было много, чтобы принять. Интересная часть еще впереди, хотя.

Прежде чем мы сможем принять наше приложение для вращения, нам нужно реализовать протокол UITableViewDataSource в классе TSPViewController . Однако именно NSFetchedResultsController вступает в игру класс NSFetchedResultsController . Чтобы убедиться, что все работает, верните 0 из tableView:numberOfRowsInSection: метод. Это приведет к пустому табличному представлению, но позволит запустить приложение без сбоев.

1
2
3
— (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    return 0;
}

Откройте класс TSPAddToDoViewController и реализуйте методы cancel: и save: как показано ниже. Мы обновим их реализации позже в этом уроке.

01
02
03
04
05
06
07
08
09
10
11
#pragma mark —
#pragma mark Actions
— (IBAction)cancel:(id)sender {
    // Dismiss View Controller
    [self dismissViewControllerAnimated:YES completion:nil];
}
 
— (IBAction)save:(id)sender {
    // Dismiss View Controller
    [self dismissViewControllerAnimated:YES completion:nil];
}

Создайте и запустите приложение в iOS Simulator, чтобы увидеть, все ли правильно подключено. Вы должны быть в состоянии нажать кнопку добавления в верхнем правом углу, чтобы вызвать TSPAddToDoViewController и отклонить последний, нажав кнопку отмены или сохранения.

Класс NSFetchedResultsController является частью инфраструктуры Core Data и предназначен для управления результатами запроса на выборку. Класс был разработан для бесперебойной работы с UITableView и UICollectionView на iOS и NSTableView на OS X. Однако его можно использовать и для других целей.

Однако прежде чем мы сможем начать работу с классом NSFetchedResultsController класс TSPViewController должен получить доступ к экземпляру NSManagedObjectContext экземпляру NSManagedObjectContext мы создали ранее в TSPViewController приложения. Начните с объявления свойства managedObjectContext типа NSManagedObjectContext в заголовочном файле класса TSPViewController .

1
2
3
4
5
6
7
8
9
#import <UIKit/UIKit.h>
 
@interface TSPViewController : UIViewController <UITableViewDataSource, UITableViewDelegate>
 
@property (weak, nonatomic) IBOutlet UITableView *tableView;
 
@property (strong, nonatomic) NSManagedObjectContext *managedObjectContext;
 
@end

Откройте Main.storyboard , выберите начальный контроллер представления раскадровки, экземпляр UINavigationController и установите для его идентификатора Storyboard значение rootNavigationController в Identity Inspector .

В приложении делегата application:didFinishLaunchingWithOptions: method мы получаем ссылку на экземпляр TSPViewController , корневой контроллер представления контроллера навигации, и устанавливаем его свойство managedObjectContext . Обновленное application:didFinishLaunchingWithOptions: метод выглядит следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
— (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    // Fetch Main Storyboard
    UIStoryboard *mainStoryboard = [UIStoryboard storyboardWithName:@»Main» bundle: nil];
     
    // Instantiate Root Navigation Controller
    UINavigationController *rootNavigationController = (UINavigationController *)[mainStoryboard instantiateViewControllerWithIdentifier:@»rootNavigationController»];
     
    // Configure View Controller
    TSPViewController *viewController = (TSPViewController *)[rootNavigationController topViewController];
     
    if ([viewController isKindOfClass:[TSPViewController class]]) {
        [viewController setManagedObjectContext:self.managedObjectContext];
    }
     
    // Configure Window
    [self.window setRootViewController:rootNavigationController];
     
    return YES;
}

Не забудьте добавить оператор импорта для класса TSPViewController в верхней части TSPAppDelegate.m .

Чтобы убедиться, что все работает, добавьте следующую инструкцию log в метод TSPViewController класса TSPViewController .

1
2
3
4
5
6
7
#pragma mark —
#pragma mark View Life Cycle
— (void)viewDidLoad {
    [super viewDidLoad];
     
    NSLog(@»%@», self.managedObjectContext);
}

Откройте файл TSPViewController класса TSPViewController и объявите свойство типа NSFetchedResultsController в расширении частного класса. Назовите свойство fetchedResultsController . Экземпляр NSFetchedResultsController также имеет свойство делегата, которое должно соответствовать протоколу NSFetchedResultsControllerDelegate . Поскольку экземпляр TSPViewController будет служить делегатом экземпляра NSFetchedResultsController , нам необходимо согласовать класс NSFetchedResultsControllerDelegate протоколом NSFetchedResultsControllerDelegate , как показано ниже.

1
2
3
4
5
6
7
8
9
#import «TSPViewController.h»
 
#import <CoreData/CoreData.h>
 
@interface TSPViewController () <NSFetchedResultsControllerDelegate>
 
@property (strong, nonatomic) NSFetchedResultsController *fetchedResultsController;
 
@end

Пришло время инициализировать экземпляр NSFetchedResultsController . Сердцем контроллера полученных результатов является объект NSFetchRequest , поскольку он определяет, какими записями будет управлять контроллер полученных результатов. В методе viewDidLoad контроллера viewDidLoad мы инициализируем запрос на выборку, передавая @"TSPItem" методу initWithEntityName: Это должно быть уже знакомо, и поэтому следующая строка, в которой мы добавляем дескрипторы сортировки в запрос на выборку, сортирует результаты на основе значения атрибута createdAt каждой записи.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
— (void)viewDidLoad {
    [super viewDidLoad];
     
    // Initialize Fetch Request
    NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] initWithEntityName:@»TSPItem»];
     
    // Add Sort Descriptors
    [fetchRequest setSortDescriptors:@[[NSSortDescriptor sortDescriptorWithKey:@»createdAt» ascending:YES]]];
     
    // Initialize Fetched Results Controller
    self.fetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:self.managedObjectContext sectionNameKeyPath:nil cacheName:nil];
     
    // Configure Fetched Results Controller
    [self.fetchedResultsController setDelegate:self];
     
    // Perform Fetch
    NSError *error = nil;
    [self.fetchedResultsController performFetch:&error];
     
    if (error) {
        NSLog(@»Unable to perform fetch.»);
        NSLog(@»%@, %@», error, error.localizedDescription);
    }
}

Инициализация контроллера полученных результатов довольно проста. initWithFetchRequest:managedObjectContext:sectionNameKeyPath:cacheName: метод принимает четыре аргумента:

  • запрос на получение
  • контекст управляемого объекта, который контролирует выбранный контроллер результатов
  • путь ключа раздела, если вы хотите, чтобы результаты были разделены на разделы
  • имя кэша, если вы хотите включить кэширование

Мы передаем nil для двух последних параметров на данный момент. Первый аргумент очевиден, но зачем нам передавать объект NSManagedObjectContext ? Мало того, что переданный в контексте управляемого объекта используется для выполнения запроса выборки, это также контекст управляемого объекта, за которым контроллер полученных результатов будет следить за изменениями. Это станет яснее через несколько минут, когда мы начнем реализовывать методы NSFetchedResultsControllerDelegate протокола NSFetchedResultsControllerDelegate .

Наконец, нам нужно указать контроллеру полученных результатов выполнить запрос на выборку, который мы передали. Мы делаем это, вызывая performFetch: и передаем ему указатель на объект NSError . Это похоже на executeFetchRequest:error: метод класса NSManagedObjectContext .

Когда настроенный контроллер результатов настроен и готов к использованию, нам необходимо реализовать протокол NSFetchedResultsControllerDelegate . Протокол определяет пять методов, три из которых представляют интерес для нас в этом руководстве:

  • controllerWillChangeContent:
  • controllerDidChangeContent:
  • controller:didChangeObject:atIndexPath:forChangeType:newIndexPath:

Первый и второй методы сообщают нам, когда изменятся данные, которыми управляет контроллер извлеченных результатов. Это важно для пакетного обновления пользовательского интерфейса. Например, вполне возможно, что несколько изменений происходят одновременно. Вместо того, чтобы обновлять пользовательский интерфейс для каждого изменения, мы пакетно обновляем пользовательский интерфейс после каждого изменения.

В нашем примере это сводится к следующим реализациям controllerWillChangeContent: и controllerDidChangeContent:

1
2
3
— (void)controllerWillChangeContent:(NSFetchedResultsController *)controller {
    [self.tableView beginUpdates];
}
1
2
3
— (void)controllerDidChangeContent:(NSFetchedResultsController *)controller {
    [self.tableView endUpdates];
}

Реализация controller:didChangeObject:atIndexPath:forChangeType:newIndexPath: немного сложнее. Этот метод делегата принимает не менее пяти аргументов:

  • экземпляр NSFetchedResultsController
  • экземпляр NSManagedObject который изменился
  • текущий путь индекса записи в контроллере полученных результатов
  • тип изменения, то есть вставка , обновление или удаление
  • новый индексный путь записи в контроллере полученных результатов после изменения

Обратите внимание, что пути индекса не имеют никакого отношения к нашему табличному представлению. NSIndexPath — это не что иное, как объект, который содержит один или несколько индексов для представления пути в иерархической структуре, отсюда и имя класса.

Внутренне контроллер извлеченных результатов управляет иерархической структурой данных и уведомляет своего делегата об изменении этой структуры данных. Мы должны визуализировать эти изменения, например, в виде таблицы.

Реализация controller:didChangeObject:atIndexPath:forChangeType:newIndexPath: выглядит устрашающе, но позвольте мне controller:didChangeObject:atIndexPath:forChangeType:newIndexPath: вам об этом.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
— (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(NSIndexPath *)newIndexPath {
    switch (type) {
        case NSFetchedResultsChangeInsert: {
            [self.tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
            break;
        }
        case NSFetchedResultsChangeDelete: {
            [self.tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
            break;
        }
        case NSFetchedResultsChangeUpdate: {
            [self configureCell:(TSPToDoCell *)[self.tableView cellForRowAtIndexPath:indexPath] atIndexPath:indexPath];
            break;
        }
        case NSFetchedResultsChangeMove: {
            [self.tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
            [self.tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
            break;
        }
    }
}

Существует четыре возможных типа изменений:

  • NSFetchedResultsChangeInsert
  • NSFetchedResultsChangeDelete
  • NSFetchedResultsChangeUpdate
  • NSFetchedResultsChangeMove

Имена довольно понятны. Если тип NSFetchedResultsChangeInsert , мы сообщаем табличному представлению вставить строку в newIndexPath . Точно так же, если тип NSFetchedResultsChangeDelete , мы удаляем строку в indexPath из табличного представления.

Если запись обновляется, мы обновляем соответствующую строку в табличном представлении, вызывая configureCell:atIndexPath: вспомогательный метод, который принимает объект UITableViewCell объект NSIndexPath . Мы реализуем этот метод в ближайшее время.

Если тип изменения равен NSFetchedResultsChangeMove , мы удаляем строку в indexPath и вставляем строку в newIndexPath чтобы отразить обновленную позицию записи во внутренней структуре данных newIndexPath контроллера результатов.

Это было не слишком сложно. Это было? Реализация протокола UITableViewDataSource намного проще, но есть несколько вещей, о которых вы должны знать. Давайте начнем с numberOfSectionsInTableView: и tableView:numberOfRowsInSection:

Несмотря на то, что табличное представление в нашем примере приложения будет иметь только один раздел, давайте спросим у контроллера полученных результатов о количестве разделов. Мы делаем это, вызывая sections , которые возвращают массив объектов, которые соответствуют протоколу NSFetchedResultsSectionInfo .

Объекты, соответствующие протоколу NSFetchedResultsSectionInfo , должны реализовывать несколько методов, включая numberOfObjects . Это дает нам то, что нам нужно для реализации первых двух методов протокола UITableViewDataSource .

1
2
3
— (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
    return [[self.fetchedResultsController sections] count];
}
1
2
3
4
5
6
— (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    NSArray *sections = [self.fetchedResultsController sections];
    id<NSFetchedResultsSectionInfo> sectionInfo = [sections objectAtIndex:section];
     
    return [sectionInfo numberOfObjects];
}

Далее идет tableView:cellForRowAtIndexPath: и configureCell:atIndexPath: методы. Начните с добавления оператора импорта для заголовка класса TSPToDoCell .

1
#import «TSPToDoCell.h»

Реализация tableView:cellForRowAtIndexPath: коротка, потому что мы переместили большую часть конфигурации ячейки в configureCell:atIndexPath: Мы просим табличное представление для повторно используемой ячейки с идентификатором повторного использования @"ToDoCell" и передаем ячейку и путь индекса в configureCell:atIndexPath:

1
2
3
4
5
6
7
8
— (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    TSPToDoCell *cell = (TSPToDoCell *)[tableView dequeueReusableCellWithIdentifier:@»ToDoCell» forIndexPath:indexPath];
     
    // Configure Table View Cell
    [self configureCell:cell atIndexPath:indexPath];
     
    return cell;
}

Волшебство происходит в configureCell:atIndexPath: Мы просим контроллер полученных результатов для элемента в indexPath . Контроллер полученных результатов возвращает нам экземпляр NSManagedObject . Мы обновляем nameLabel и состояние doneButton , запрашивая у записи его name и атрибуты done .

1
2
3
4
5
6
7
8
— (void)configureCell:(TSPToDoCell *)cell atIndexPath:(NSIndexPath *)indexPath {
    // Fetch Record
    NSManagedObject *record = [self.fetchedResultsController objectAtIndexPath:indexPath];
     
    // Update Cell
    [cell.nameLabel setText:[record valueForKey:@»name»]];
    [cell.doneButton setSelected:[[record valueForKey:@»done»] boolValue]];
}

Мы вернемся к протоколу UITableViewDataSource позже в этом руководстве, когда будем удалять элементы из списка. Сначала нам нужно заполнить табличное представление некоторыми данными.

Давайте закончим этот урок, добавив возможность создавать задачи. Откройте класс TSPAddToDoViewController , добавьте оператор импорта для платформы Core Data и объявите свойство managedObjectContext типа NSManagedObjectContext .

01
02
03
04
05
06
07
08
09
10
11
#import <UIKit/UIKit.h>
 
#import <CoreData/CoreData.h>
 
@interface TSPAddToDoViewController : UIViewController <UITextFieldDelegate>
 
@property (weak, nonatomic) IBOutlet UITextField *textField;
 
@property (strong, nonatomic) NSManagedObjectContext *managedObjectContext;
 
@end

Вернитесь к классу TSPViewController и реализуйте метод prepareForSegue:sender: В этом методе мы устанавливаем свойство managedObjectContext экземпляра TSPAddToDoViewController . Если вы раньше работали с раскадровками, то реализация prepareForSegue:sender: должна быть простой.

01
02
03
04
05
06
07
08
09
10
— (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
    if ([segue.identifier isEqualToString:@»addToDoViewController»]) {
        // Obtain Reference to View Controller
        UINavigationController *nc = (UINavigationController *)[segue destinationViewController];
        TSPAddToDoViewController *vc = (TSPAddToDoViewController *)[nc topViewController];
         
        // Configure View Controller
        [vc setManagedObjectContext:self.managedObjectContext];
    }
}

Если пользователь вводит текст в текстовое поле TSPAddToDoViewController и нажимает кнопку « Сохранить» , мы создаем новую запись, заполняем ее данными и сохраняем ее. Эта логика входит в метод save: который мы создали ранее.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
— (IBAction)save:(id)sender {
    // Helpers
    NSString *name = self.textField.text;
     
    if (name && name.length) {
        // Create Entity
        NSEntityDescription *entity = [NSEntityDescription entityForName:@»TSPItem» inManagedObjectContext:self.managedObjectContext];
         
        // Initialize Record
        NSManagedObject *record = [[NSManagedObject alloc] initWithEntity:entity insertIntoManagedObjectContext:self.managedObjectContext];
         
        // Populate Record
        [record setValue:name forKey:@»name»];
        [record setValue:[NSDate date] forKey:@»createdAt»];
         
        // Save Record
        NSError *error = nil;
         
        if ([self.managedObjectContext save:&error]) {
            // Dismiss View Controller
            [self dismissViewControllerAnimated:YES completion:nil];
             
        } else {
            if (error) {
                NSLog(@»Unable to save record.»);
                NSLog(@»%@, %@», error, error.localizedDescription);
            }
             
            // Show Alert View
            [[[UIAlertView alloc] initWithTitle:@»Warning» message:@»Your to-do could not be saved.»
        }
         
    } else {
        // Show Alert View
        [[[UIAlertView alloc] initWithTitle:@»Warning» message:@»Your to-do needs a name.»
    }
}

Метод save: выглядит довольно впечатляюще, но там нет ничего, что мы еще не рассмотрели. Мы создаем новый управляемый объект, передавая экземпляр NSEntityDescription экземпляр NSManagedObjectContext . Затем мы заполняем управляемый объект именем и датой. Если сохранение контекста управляемого объекта прошло успешно, мы отклоняем контроллер представления, в противном случае мы показываем представление с предупреждением. Если пользователь нажимает кнопку сохранения, не вводя текст, мы также показываем предупреждение.

Запустите приложение и добавьте несколько элементов. Я уверен, что вы согласны с тем, что класс NSFetchedResultsController делает процесс добавления элементов невероятно простым. Он заботится о мониторинге контекста управляемого объекта на предмет изменений, и мы обновляем пользовательский интерфейс, табличное представление класса TSPViewController , основываясь на том, что экземпляр NSFetchedResultsController сообщает нам через протокол NSFetchedResultsControllerDelegate .

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