В этом руководстве мы продолжим исследование класса NSFetchedResultsController , добавив возможность обновлять и удалять элементы NSFetchedResultsController дел. Вы заметите, что обновление и удаление текущих дел на удивление легко благодаря основам, которые мы заложили в предыдущем уроке .
1. Обновление имени записи
Шаг 1: Создать View Controller
Начните с создания нового подкласса TSPUpdateToDoViewController именем TSPUpdateToDoViewController . В TSPUpdateToDoViewController.h объявите выход, textField типа UITextField и два свойства managedObjectContext типа NSManagedObjectContext и record типа NSManagedObject . Добавьте оператор импорта для базовой структуры данных вверху.
|
01
02
03
04
05
06
07
08
09
10
11
12
13
|
#import <UIKit/UIKit.h>
#import <CoreData/CoreData.h>
@interface TSPUpdateToDoViewController : UIViewController
@property (weak, nonatomic) IBOutlet UITextField *textField;
@property (strong, nonatomic) NSManagedObjectContext *managedObjectContext;
@property (strong, nonatomic) NSManagedObject *record;
@end
|
В файле реализации контроллера представления, TSPUpdateToDoViewController.m , создайте два действия cancel: и save: Их реализации могут пока оставаться пустыми.
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
|
#import «TSPUpdateToDoViewController.h»
@implementation TSPUpdateToDoViewController
#pragma mark —
#pragma mark Actions
— (IBAction)cancel:(id)sender {
}
— (IBAction)save:(id)sender {
}
@end
|
Шаг 2: Обновить раскадровку
Откройте основную раскадровку Main.storyboard , добавьте новый объект контроллера представления и установите для его класса значение TSPUpdateToDoViewController в Identity Inspector . Создайте ручной переход от класса TSPUpdateToDoViewController классу TSPUpdateToDoViewController . В инспекторе атрибутов установите стиль перехода для push и его идентификатор для updateToDoViewController .
Добавьте объект UITextField в представление объекта TSPUpdateToDoViewController и настройте его так же, как мы это делали с текстовым полем класса TSPAddToDoViewController . Не забудьте соединить розетку контроллера вида с текстовым полем.
Как и в классе TSPAddToDoViewController , добавьте два элемента кнопок панели на панель навигации контроллера представления, установите для их идентификаторов соответственно значение Отмена и Сохранить и подключите каждый элемент кнопки панели к соответствующему действию в Инспекторе соединений .

Шаг 3: Передача ссылки
Нам также нужно внести несколько изменений в класс TSPViewController . Добавьте оператор импорта для класса TSPUpdateToDoViewController вверху и объявите свойство с именем selection типа NSIndexPath к расширению частного класса в TSPViewController.m .
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
|
#import «TSPViewController.h»
#import <CoreData/CoreData.h>
#import «TSPToDoCell.h»
#import «TSPAddToDoViewController.h»
#import «TSPUpdateToDoViewController.h»
@interface TSPViewController () <NSFetchedResultsControllerDelegate>
@property (strong, nonatomic) NSFetchedResultsController *fetchedResultsController;
@property (strong, nonatomic) NSIndexPath *selection;
@end
|
Затем реализуем tableView:didSelectRowAtIndexPath: метод протокола UITableViewDelegate . В этом методе мы временно сохраняем выбор пользователя в свойстве selection .
|
1
2
3
4
5
6
7
8
|
#pragma mark —
#pragma mark Table View Delegate Methods
— (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
[tableView deselectRowAtIndexPath:indexPath animated:YES];
// Store Selection
[self setSelection:indexPath];
}
|
В классе prepareForSegue:sender: мы выбираем запись, соответствующую выбору пользователя, и передаем ее в экземпляр TSPUpdateToDoViewController . Чтобы предотвратить непредвиденное поведение, мы выполняем этот шаг, только если выбор не равен nil и сбрасываем свойство selection после выборки записи из контроллера полученных результатов.
|
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
|
— (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];
} else if ([segue.identifier isEqualToString:@»updateToDoViewController»]) {
// Obtain Reference to View Controller
TSPUpdateToDoViewController *vc = (TSPUpdateToDoViewController *)[segue destinationViewController];
// Configure View Controller
[vc setManagedObjectContext:self.managedObjectContext];
if (self.selection) {
// Fetch Record
NSManagedObject *record = [self.fetchedResultsController objectAtIndexPath:self.selection];
if (record) {
[vc setRecord:record];
}
// Reset Selection
[self setSelection:nil];
}
}
}
|
Шаг 4: Заполнение текстового поля
В методе TSPUpdateToDoViewController класса TSPUpdateToDoViewController заполните текстовое поле именем записи, как показано ниже.
|
01
02
03
04
05
06
07
08
09
10
|
#pragma mark —
#pragma mark View Life Cycle
— (void)viewDidLoad {
[super viewDidLoad];
if (self.record) {
// Update Text Field
[self.textField setText:[self.record valueForKey:@»name»]];
}
}
|
Шаг 5: Обновление записи
В действии cancel: мы извлекаем контроллер обновлений из стека навигации контроллера навигации.
|
1
2
3
4
|
— (IBAction)cancel:(id)sender {
// Pop View Controller
[self.navigationController popViewControllerAnimated:YES];
}
|
В действии save: мы сначала проверяем, является ли текстовое поле пустым, и показываем представление предупреждения, если оно есть. Если текстовое поле содержит допустимое значение, мы обновляем атрибут name записи и извлекаем контроллер представления из стека навигации контроллера навигации.
|
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
|
— (IBAction)save:(id)sender {
// Helpers
NSString *name = self.textField.text;
if (name && name.length) {
// Populate Record
[self.record setValue:name forKey:@»name»];
// Save Record
NSError *error = nil;
if ([self.managedObjectContext save:&error]) {
// Pop View Controller
[self.navigationController popViewControllerAnimated:YES];
} 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.»
}
}
|
Это все, что нужно для обновления записи с использованием Core Data. Запустите приложение еще раз и посмотрите, все ли работает. Контроллер полученных результатов автоматически обнаруживает изменение и уведомляет его делегата, экземпляр TSPViewController . Объект TSPViewController , в свою очередь, обновляет табличное представление, чтобы отразить изменение. Это так просто.
2. Обновление состояния записи
Шаг 1: Обновление TSPToDoCell
Когда пользователь нажимает кнопку справа от TSPToDoCell , состояние элемента должно измениться. Для этого нам сначала нужно обновить класс TSPToDoCell . Откройте TSPToDoCell.m и добавьте typedef для блока с именем TSPToDoCellDidTapButtonBlock . Затем объявите свойство типа TSPToDoCellDidTapButtonBlock и убедитесь, что свойство копируется при назначении.
|
01
02
03
04
05
06
07
08
09
10
11
12
|
#import <UIKit/UIKit.h>
typedef void (^TSPToDoCellDidTapButtonBlock)();
@interface TSPToDoCell : UITableViewCell
@property (weak, nonatomic) IBOutlet UILabel *nameLabel;
@property (weak, nonatomic) IBOutlet UIButton *doneButton;
@property (copy, nonatomic) TSPToDoCellDidTapButtonBlock didTapButtonBlock;
@end
|
Перейдите к файлу реализации класса, TSPToDoCell.m , и вызовите вспомогательный метод awakeFromNib в awakeFromNib .
|
1
2
3
4
5
6
7
8
|
#pragma mark —
#pragma mark Initialization
— (void)awakeFromNib {
[super awakeFromNib];
// Setup View
[self setupView];
}
|
В setupView мы настраиваем объект doneButton , устанавливая изображения для каждого состояния кнопки и добавляя ячейку табличного представления в качестве цели. Когда пользователь нажимает кнопку, ячейке табличного представления отправляется сообщение didTapButton: в котором мы didTapButtonBlock блок didTapButtonBlock . Через мгновение вы увидите, насколько удобен этот шаблон. Изображения включены в исходные файлы этого урока, которые вы можете найти на GitHub .
|
01
02
03
04
05
06
07
08
09
10
11
12
|
#pragma mark —
#pragma mark View Methods
— (void)setupView {
UIImage *imageNormal = [UIImage imageNamed:@»button-done-normal»];
UIImage *imageSelected = [UIImage imageNamed:@»button-done-selected»];
[self.doneButton setImage:imageNormal forState:UIControlStateNormal];
[self.doneButton setImage:imageNormal forState:UIControlStateDisabled];
[self.doneButton setImage:imageSelected forState:UIControlStateSelected];
[self.doneButton setImage:imageSelected forState:UIControlStateHighlighted];
[self.doneButton addTarget:self action:@selector(didTapButton:) forControlEvents:UIControlEventTouchUpInside];
}
|
|
1
2
3
4
5
6
7
|
#pragma mark —
#pragma mark Actions
— (void)didTapButton:(UIButton *)button {
if (self.didTapButtonBlock) {
self.didTapButtonBlock();
}
}
|
Шаг 2: Обновление TSPViewController
Благодаря классу NSFetchedResultsController и заложенному нами фундаменту нам нужно всего лишь обновить configureCell:atIndexPath: метод в классе TSPViewController .
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
|
— (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]];
[cell setDidTapButtonBlock:^{
BOOL isDone = [[record valueForKey:@»done»] boolValue];
// Update Record
[record setValue:@(!isDone) forKey:@»done»];
}];
}
|
Шаг 3: Сохранение изменений
Вы можете быть удивлены, почему мы не сохраняем контекст управляемого объекта. Не потеряем ли мы внесенные нами изменения, если не внесем изменения в постоянное хранилище? Да и нет.
Это правда, что в какой-то момент нам нужно записать изменения контекста управляемого объекта в резервное хранилище. Если мы этого не сделаем, пользователь потеряет часть своих данных. Однако нет необходимости сохранять изменения контекста управляемого объекта каждый раз, когда мы вносим изменения.
Лучший подход — сохранить контекст управляемого объекта в тот момент, когда приложение переходит в фоновый режим. Мы можем сделать это в applicationDidEnterBackground: метод протокола UIApplicationDelegate . Откройте TSPAppDelegate.m и реализуйте applicationDidEnterBackground: как показано ниже.
|
01
02
03
04
05
06
07
08
09
10
|
— (void)applicationDidEnterBackground:(UIApplication *)application {
NSError *error = nil;
if (![self.managedObjectContext save:&error]) {
if (error) {
NSLog(@»Unable to save changes.»);
NSLog(@»%@, %@», error, error.localizedDescription);
}
}
}
|
Однако это не работает, если пользователь принудительно завершает приложение. Поэтому хорошей идеей также является сохранение контекста управляемого объекта при завершении работы приложения. applicationWillTerminate: метод — это еще один метод протокола UIApplicationDelegate , который уведомляет делегат приложения о том, что приложение должно быть завершено.
|
01
02
03
04
05
06
07
08
09
10
|
— (void)applicationWillTerminate:(UIApplication *)application {
NSError *error = nil;
if (![self.managedObjectContext save:&error]) {
if (error) {
NSLog(@»Unable to save changes.»);
NSLog(@»%@, %@», error, error.localizedDescription);
}
}
}
|
Обратите внимание, что у нас есть повторяющийся код в applicationDidEnterBackground: и applicationWillTerminate: Поэтому хорошей идеей является создание вспомогательного метода для сохранения контекста управляемого объекта и вызова этого вспомогательного метода в обоих методах делегата.
|
01
02
03
04
05
06
07
08
09
10
11
12
|
#pragma mark —
#pragma mark Helper Methods
— (void)saveManagedObjectContext {
NSError *error = nil;
if (![self.managedObjectContext save:&error]) {
if (error) {
NSLog(@»Unable to save changes.»);
NSLog(@»%@, %@», error, error.localizedDescription);
}
}
}
|
3. Удаление записей
Вы будете удивлены, насколько легко удалить записи с NSFetchedResultsController класса NSFetchedResultsController . Начните с реализации tableView:canEditRowAtIndexPath: метод протокола UITableViewDataSource .
|
1
2
3
|
— (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath {
return YES;
}
|
Второй метод протокола UITableViewDataSource , который нам нужно реализовать, — это tableView:commitEditingStyle:forRowAtIndexPath: В этом методе мы выбираем управляемый объект, выбранный пользователем для удаления, и передаем его в deleteObject: метод контекста управляемого объекта контроллера извлеченных результатов.
|
1
2
3
4
5
6
7
8
9
|
— (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath {
if (editingStyle == UITableViewCellEditingStyleDelete) {
NSManagedObject *record = [self.fetchedResultsController objectAtIndexPath:indexPath];
if (record) {
[self.fetchedResultsController.managedObjectContext deleteObject:record];
}
}
}
|
Поскольку мы уже реализовали протокол NSFetchedResultsControllerDelegate , пользовательский интерфейс автоматически обновляется, включая анимацию.
Вывод
Я надеюсь, что вы согласны с тем, что класс NSFetchedResultsController является очень удобным членом инфраструктуры Core Data. Если вы понимаете основы инфраструктуры базовых данных, то освоить этот класс несложно. Я призываю вас дополнительно изучить его API, чтобы узнать, что еще он может сделать для вас.