В этом руководстве мы продолжим исследование класса 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, чтобы узнать, что еще он может сделать для вас.