Статьи

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

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

Начните с создания нового подкласса 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

Откройте основную раскадровку Main.storyboard , добавьте новый объект контроллера представления и установите для его класса значение TSPUpdateToDoViewController в Identity Inspector . Создайте ручной переход от класса TSPUpdateToDoViewController классу TSPUpdateToDoViewController . В инспекторе атрибутов установите стиль перехода для push и его идентификатор для updateToDoViewController .

Добавьте объект UITextField в представление объекта TSPUpdateToDoViewController и настройте его так же, как мы это делали с текстовым полем класса TSPAddToDoViewController . Не забудьте соединить розетку контроллера вида с текстовым полем.

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

Нам также нужно внести несколько изменений в класс 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];
        }
    }
}

В методе 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»]];
    }
}

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

Когда пользователь нажимает кнопку справа от 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();
    }
}

Благодаря классу 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»];
    }];
}

Вы можете быть удивлены, почему мы не сохраняем контекст управляемого объекта. Не потеряем ли мы внесенные нами изменения, если не внесем изменения в постоянное хранилище? Да и нет.

Это правда, что в какой-то момент нам нужно записать изменения контекста управляемого объекта в резервное хранилище. Если мы этого не сделаем, пользователь потеряет часть своих данных. Однако нет необходимости сохранять изменения контекста управляемого объекта каждый раз, когда мы вносим изменения.

Лучший подход — сохранить контекст управляемого объекта в тот момент, когда приложение переходит в фоновый режим. Мы можем сделать это в 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);
        }
    }
}

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