В предыдущих выпусках этой серии мы рассмотрели основы инфраструктуры основных данных. Пришло время использовать наши знания, создав простое приложение на базе Core Data.
В этом уроке мы также познакомимся с другим звездным игроком платформы Core Data, классом NSFetchedResultsController
. Приложение, которое мы собираемся создать, управляет списком задач. С помощью приложения мы можем добавлять, обновлять и удалять задачи. Вы быстро поймете, что класс NSFetchedResultsController
делает это очень легко.
1. Настройка проекта
Откройте Xcode, выберите « Создать»> «Проект …» в меню « Файл» и выберите шаблон приложения « Один вид» в разделе « iOS»> « Категория приложения ».
Назовите проект « Готово» , установите « Устройства» на iPhone и скажите Xcode, где вы хотите сохранить проект.
Поскольку мы выбрали шаблон приложения Single View , XCode не создал для нас настройку Core Data. Однако настроить стек основных данных должно быть легко, если вы читали предыдущие части этой серии.
2. Настройка основных данных
Откройте файл реализации класса делегата приложения, 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
. Мы еще вернемся к проблемам миграции и несовместимости позже в этой серии.
Хотя наше приложение не будет зависать, если вы попытаетесь запустить его, стек основных данных не будет настроен должным образом. Причина проста, мы еще не создали модель данных. Давайте позаботимся об этом на следующем шаге.
3. Создание модели данных
Выберите « Создать»> «Файл …» в меню « Файл» и выберите « Модель данных» в категории « iOS»> «Основные данные ».
Назовите файл Done , дважды проверьте, что он добавлен в цель Done , и скажите Xcode, где его нужно сохранить.
Модель данных будет очень простой. Создайте новый объект и назовите его TSPItem . Добавьте три атрибута к сущности, name
типа String , createdAt
с типом Date и done
с типом Boolean .
Отметьте атрибуты как обязательные, а не необязательные, и установите значение по умолчанию для атрибута done
как NO
.
Стек основных данных настроен, а модель данных настроена правильно. Пришло время познакомиться с новым классом инфраструктуры Core Data, классом NSFetchedResultsController
.
4. Управление данными
Эта статья не только о классе NSFetchedResultsController
, но и о том, что класс NSFetchedResultsController
делает за кулисами. Позвольте мне уточнить, что я имею в виду под этим.
Если бы мы собирали наше приложение без класса NSFetchedResultsController
, нам нужно было бы найти способ синхронизации пользовательского интерфейса приложения с данными, управляемыми Core Data. К счастью, у Core Data есть элегантное решение этой проблемы.
Всякий раз, когда запись вставляется , обновляется или удаляется в контексте управляемого объекта, контекст управляемого объекта отправляет уведомление через центр уведомлений. Контекст управляемого объекта может публиковать три типа уведомлений:
-
NSManagedObjectContextObjectsDidChangeNotification
: это уведомление публикуется каждый раз, когда запись в контексте управляемого объекта вставляется, обновляется или удаляется. -
NSManagedObjectContextWillSaveNotification
: это уведомление публикуется контекстом управляемого объекта до того, как ожидающие изменения фиксируются в резервном хранилище. -
NSManagedObjectContextDidSaveNotification
: это уведомление публикуется контекстом управляемого объекта сразу после ожидающих изменений в резервном хранилище.
Содержимое этих уведомлений идентично, то есть свойство object
уведомления — это экземпляр NSManagedObjectContext
который разместил уведомление, а словарь userInfo
уведомления содержит записи, которые были вставлены , обновлены и удалены .
Суть в том, что для поддержания результатов запроса на получение обновлений требуется значительное количество стандартного кода. Другими словами, если бы мы создавали наше приложение без использования класса NSFetchedResultsController
, нам пришлось бы реализовать механизм, который отслеживал бы изменения в контексте управляемого объекта, и соответственно обновлять пользовательский интерфейс. Давайте посмотрим, как NSFetchedResultsController
может помочь нам в этой задаче.
5. Настройка интерфейса пользователя
Работать с классом 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
.
Шаг 1: Заполнение раскадровки
Откройте основную раскадровку проекта 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
в Инспекторе Атрибутов . Это было много, чтобы принять. Интересная часть еще впереди, хотя.
Шаг 2: Реализация табличного представления
Прежде чем мы сможем принять наше приложение для вращения, нам нужно реализовать протокол UITableViewDataSource
в классе TSPViewController
. Однако именно NSFetchedResultsController
вступает в игру класс NSFetchedResultsController
. Чтобы убедиться, что все работает, верните 0
из tableView:numberOfRowsInSection:
метод. Это приведет к пустому табличному представлению, но позволит запустить приложение без сбоев.
1
2
3
|
— (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return 0;
}
|
Шаг 3: Сохранить и отменить
Откройте класс 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
и отклонить последний, нажав кнопку отмены или сохранения.
6. Реализация класса NSFetchedResultsController
Класс NSFetchedResultsController
является частью инфраструктуры Core Data и предназначен для управления результатами запроса на выборку. Класс был разработан для бесперебойной работы с UITableView
и UICollectionView
на iOS и NSTableView
на OS X. Однако его можно использовать и для других целей.
Шаг 1: заложить основу
Однако прежде чем мы сможем начать работу с классом 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);
}
|
Шаг 2. Инициализация экземпляра NSFetchedResultsController
Откройте файл 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
.
Шаг 3: Реализация протокола делегата
Когда настроенный контроллер результатов настроен и готов к использованию, нам необходимо реализовать протокол 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
контроллера результатов.
Шаг 4: Реализация протокола UITableViewDataSource
Это было не слишком сложно. Это было? Реализация протокола 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
позже в этом руководстве, когда будем удалять элементы из списка. Сначала нам нужно заполнить табличное представление некоторыми данными.
7. Добавление записей
Давайте закончим этот урок, добавив возможность создавать задачи. Откройте класс 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 транслирует изменения состояния контекста управляемого объекта, имеет важное значение, поэтому убедитесь, что вы это понимаете, прежде чем двигаться дальше.