Статьи

Основные данные с нуля: создание подклассов NSManagedObject

Ранее в этой серии мы создали простое приложение Done , чтобы узнать больше о классе NSFetchedResultsController . В этом проекте мы использовали кодирование значения ключа (KVC) и наблюдение значения ключа (KVO) для создания и обновления записей. Это прекрасно работает, но с того момента, как ваш проект будет иметь какую-либо сложность, вы быстро столкнетесь с проблемами. Синтаксис KVC не только многословен, valueForKey: и setValue:forKey: он также может привести к ошибкам, которые являются результатом опечаток. Следующий фрагмент кода является хорошим примером последней проблемы.

1
2
3
4
[record setValue:[NSDate date] forKey:@»createdat»];
[record setValue:[NSDate date] forKey:@»CreatedAt»];
[record setValue:[NSDate date] forKey:@»createdAt»];
[record setValue:[NSDate date] forKey:@»CREATEDAT»];

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

Вышеуказанная проблема легко решается с помощью строковых констант, но я не пытаюсь это сделать. Кодирование значения ключа — это здорово, но оно многословно и его трудно читать, если вы привыкли к точечному синтаксису Objective-C. Чтобы упростить работу с экземплярами NSManagedObject , лучше создать подкласс NSManagedObject для каждой сущности модели данных, и об этом вы узнаете в этой статье.

Чтобы сэкономить время, мы вернемся к Done , приложению, которое мы создали ранее в этой серии. Загрузите его с GitHub и откройте в Xcode.

Создать подкласс NSManagedObject очень легко. Хотя можно вручную создать подкласс NSManagedObject для сущности, проще позволить XCode выполнить всю работу за вас.

Откройте модель данных проекта Done.xcdatamodeld и выберите объект TSPItem . Выберите New> File … из меню File Xcode, выберите шаблон подкласса NSManagedObject из раздела Core Data и нажмите Next .

Установите флажок правильной модели данных, Готово , в списке моделей данных и нажмите Далее .

На следующем шаге вас попросят выбрать сущности, для которых вы хотите создать подкласс NSManagedObject . Проверьте флажок объекта TSPItem и нажмите Далее .

Выберите место для хранения файлов классов подкласса NSManagedObject и убедитесь, что параметр Использовать скалярные свойства для примитивных типов данных не отмечен. Я объясню значение этой опции через несколько минут. Нажмите кнопку Создать, чтобы создать подкласс NSManagedObject для сущности TSPItem .

Перейдите к файлам Xcode, созданным для вас, и посмотрите на их содержимое. Файл заголовка TSPItem.h должен выглядеть примерно так, как показано ниже.

01
02
03
04
05
06
07
08
09
10
#import <Foundation/Foundation.h>
#import <CoreData/CoreData.h>
 
@interface TSPItem : NSManagedObject
 
@property (nonatomic, retain) NSDate * createdAt;
@property (nonatomic, retain) NSNumber * done;
@property (nonatomic, retain) NSString * name;
 
@end

В верхней части вы должны увидеть операторы импорта для основ и базовых структур данных . Подкласс NSManagedObject содержит три свойства, соответствующие атрибутам объекта TSPItem модели данных. Хотя есть несколько отличий.

Типы name и свойств createdAt , NSString , не удивительны. Тип свойства done , однако, менее очевиден. Несмотря на то, что мы объявили тип атрибута done как логическое значение в модели данных, свойство done имеет тип NSNumber . Причина проста. Когда мы создали подкласс NSManagedObject несколько минут назад, мы оставили флажок Использовать скалярные свойства для примитивных типов данных не отмеченным. Если бы мы установили этот флажок, свойство done было бы типа BOOL .

Хотя вы сами решаете, хотите ли вы работать с объектами или примитивными типами данных, есть причина, по которой флажок снят по умолчанию. Вы быстро обнаружите, что менее удобно использовать примитивные типы данных для свойств подкласса NSManagedObject . Это может хорошо выглядеть на бумаге, но это становится проблемой, когда вы, например, хотите хранить примитивные типы данных в коллекциях Objective C, которые принимают только объекты.

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

1
2
3
4
5
6
7
8
9
#import «TSPItem.h»
 
@implementation TSPItem
 
@dynamic createdAt;
@dynamic done;
@dynamic name;
 
@end

Используя директиву @dynamic , компилятор знает, что @dynamic доступа (методы получения и установки) для свойств, объявленных в интерфейсе класса, будут созданы во время выполнения. Компилятор берет на себя это слово и не выдает никаких предупреждений.

Если вы закомментируете операторы @dynamic , компилятор @dynamic три предупреждения, сообщающих нам, что средства доступа к этим трем свойствам отсутствуют.

С готовым к использованию классом TSPItem пришло время обновить проект, заменив все вхождения valueForKey: и setValue:forKey:

Откройте файл TSPViewController класса TSPViewController и начните с добавления оператора импорта для класса TSPItem вверху.

1
#import «TSPItem.h»

Перейдите к configureCell:atIndexPath: и обновите реализацию, как показано ниже.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
— (void)configureCell:(TSPToDoCell *)cell atIndexPath:(NSIndexPath *)indexPath {
    // Fetch Record
    TSPItem *record = [self.fetchedResultsController objectAtIndexPath:indexPath];
     
    // Update Cell
    [cell.nameLabel setText:record.name];
    [cell.doneButton setSelected:[record.done boolValue]];
     
    [cell setDidTapButtonBlock:^{
        BOOL isDone = [record.done boolValue];
         
        // Update Record
        record.done = @(!isDone);
    }];
}

Мы сделали пять изменений. Сначала мы изменили тип переменной record на TSPItem . objectAtIndexPath: метод класса NSFetchedResultsController возвращает экземпляр класса NSManagedObject . Это все еще верно, поскольку класс TSPItem является подклассом NSManagedObject .

Мы также подставляем valueForKey: звонки. Вместо этого мы используем свойства объекта record , name и done . Благодаря точечному синтаксису Objective-C результат очень разборчивый. Обратите внимание, что мы вызываем boolValue done свойства done . Помните, что свойство done имеет тип NSNumber , поэтому нам нужно вызвать boolValue для него, чтобы получить фактическое логическое значение. Чтобы установить значение переменной isDone , мы повторяем этот шаг.

Чтобы обновить запись, мы больше не вызываем setValue:forKey: Вместо этого мы используем точечный синтаксис, чтобы установить свойство done . Я уверен, что вы согласны с тем, что это намного элегантнее, чем использование прямого кодирования значения ключа.

Помните, что KVC и KVO остаются неотъемлемой частью Core Data. Базовые данные используют valueForKey: и setValue:forKey: чтобы выполнить работу.

Нам также нужно внести несколько изменений в класс TSPAddToDoViewController . Начните с добавления оператора импорта для класса TSPItem . В методе save: сначала нам нужно обновить инициализацию экземпляра NSManagedObject . Вместо инициализации экземпляра NSManagedObject мы создаем экземпляр TSPItem .

1
2
// Initialize Record
TSPItem *record = [[TSPItem alloc] initWithEntity:entity insertIntoManagedObjectContext:self.managedObjectContext];

Чтобы заполнить запись, мы используем точечный синтаксис вместо метода setValue:forKey как показано ниже.

1
2
3
// Populate Record
record.name = name;
record.createdAt = [NSDate date];

Последний класс, который нам нужно обновить, — это класс TSPUpdateToDoViewController . Давайте начнем с интерфейса класса. Откройте TSPUpdateToDoViewController.h и обновите его содержимое, как показано ниже. Мы добавляем прямое объявление класса для класса TSPItem вверху и меняем тип свойства record на TSPItem .

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

Это изменение приведет к предупреждению в классе TSPViewController . Чтобы увидеть, что не так, откройте файл TSPViewController класса TSPViewController и перейдите к prepareForSegue:sender:

Мы запрашиваем у контроллера полученных результатов запись по выбранному пути индекса, self.selection . Тип переменной recordNSManagedObject , но класс TSPUpdateToDoViewController ожидает экземпляр TSPItem . Решение очень простое, как вы можете видеть ниже.

01
02
03
04
05
06
07
08
09
10
11
if (self.selection) {
    // Fetch Record
    TSPItem *record = [self.fetchedResultsController objectAtIndexPath:self.selection];
     
    if (record) {
        [vc setRecord:record];
    }
     
    // Reset Selection
    [self setSelection:nil];
}

Вернитесь к классу TSPUpdateToDoViewController и добавьте оператор импорта для класса TSPItem в верхней части файла его реализации. Обновите метод viewDidLoad как показано ниже.

1
2
3
4
5
6
7
8
— (void)viewDidLoad {
    [super viewDidLoad];
     
    if (self.record) {
        // Update Text Field
        [self.textField setText:self.record.name];
    }
}

Нам также необходимо обновить метод save: в котором мы заменим setValue:forKey: синтаксисом точки.

1
2
// Populate Record
self.record.name = name;

Создайте проект и запустите приложение в iOS Simulator, чтобы увидеть, все ли работает по-прежнему.

Текущая модель данных не содержит никаких отношений, но давайте добавим несколько, чтобы увидеть, как выглядит подкласс NSManagedObject со связями. Как мы видели в предыдущей статье этой серии, нам сначала нужно создать новую версию модели данных. Выберите модель данных в Навигаторе проекта и выберите « Добавить версию модели …» в меню « Редактор» . Установите для Version Version значение Done 2 и основывайте модель на текущей модели данных, Done . Нажмите Готово, чтобы создать новую версию модели данных.

Откройте Done 2.xcdatamodel , создайте новый объект с именем TSPUser и добавьте имя атрибута типа String . Добавьте элементы отношений и установите пункт назначения TSPItem . Оставьте пока обратное отношение пустым. Выбрав взаимосвязь элементов , откройте инспектор модели данных справа и установите для типа взаимосвязи значение « Многие» . Пользователь может иметь более одного элемента, связанного с ним.

Выберите объект TSPItem , создайте пользователя взаимосвязи и установите для адресата значение TSPUser . Установите обратное отношение к элементам . Это автоматически установит обратную связь отношения элементов сущности TSPUser .

Прежде чем мы создадим подклассы NSManagedObject для обеих сущностей, нам нужно сообщить модели данных, какую версию модели данных она должна использовать. Выберите Done.xcdatamodeld , откройте инспектор файлов справа и установите текущую версию модели на Done 2 . Когда вы сделаете это, дважды проверьте, что вы выбрали Done.xcdatamodeld , а не Done.xcdatamodel .

Удалите TSPItem.h и TSPItem.m из проекта. Выберите New> File … в меню File и выберите шаблон подкласса NSManagedObject в разделе Core Data . В списке моделей данных выберите Готово 2 .

Выберите обе сущности из списка сущностей. Поскольку мы изменили сущность TSPItem , нам нужно заново создать соответствующий подкласс NSManagedObject .

Давайте сначала посмотрим на изменения недавно сгенерированного класса TSPItem . Как вы можете видеть ниже, заголовочный файл содержит одно дополнительное свойство user типа TSPUser . Чтобы удовлетворить компилятор, Xcode также добавил прямое объявление класса вверху.

01
02
03
04
05
06
07
08
09
10
11
12
13
#import <Foundation/Foundation.h>
#import <CoreData/CoreData.h>
 
@class TSPUser;
 
@interface TSPItem : NSManagedObject
 
@property (nonatomic, retain) NSDate * createdAt;
@property (nonatomic, retain) NSNumber * done;
@property (nonatomic, retain) NSString * name;
@property (nonatomic, retain) TSPUser *user;
 
@end

Файл реализации отражает добавление user свойства.

01
02
03
04
05
06
07
08
09
10
11
#import «TSPItem.h»
#import «TSPUser.h»
 
@implementation TSPItem
 
@dynamic createdAt;
@dynamic done;
@dynamic name;
@dynamic user;
 
@end

Вот как выглядит отношение To One в подклассе NSManagedObject . Xcode достаточно умен, чтобы сделать вывод, что тип user свойства — TSPUser , подкласс NSManagedObject мы создали недавно.

Класс TSPUser немного сложнее. Посмотрите на интерфейс класса. Первое, что вы заметите, это то, что тип свойства itemsNSSet . Это не должно быть сюрпризом, потому что мы уже знали, что Core Data использует класс NSSet для хранения членов To Many отношения.

01
02
03
04
05
06
07
08
09
10
#import <Foundation/Foundation.h>
#import <CoreData/CoreData.h>
 
@class TSPItem;
 
@interface TSPUser : NSManagedObject
 
@property (nonatomic, retain) NSString * name;
@property (nonatomic, retain) NSSet *items;
@end

Заголовочный файл класса TSPUser также содержит расширение класса, включающее четыре TSPUser метода. Это еще одно преимущество использования подкласса NSManagedObject и позволяет Xcode генерировать его для нас. Вместо того, чтобы напрямую манипулировать свойством items , мы можем добавлять и удалять экземпляры TSPItem используя эти удобные методы.

1
2
3
4
5
6
7
8
@interface TSPUser (CoreDataGeneratedAccessors)
 
— (void)addItemsObject:(TSPItem *)value;
— (void)removeItemsObject:(TSPItem *)value;
— (void)addItems:(NSSet *)values;
— (void)removeItems:(NSSet *)values;
 
@end

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

01
02
03
04
05
06
07
08
09
10
11
/**
 * With Convenience Methods
 */
[user addItemsObject:item];
 
/**
 * Without Convenience Methods
 */
NSMutableSet *items = [[user items] mutableCopy];
[items addObject:item];
[user setItems:items];

Однако в рамках Core Data используется кодирование значения ключа для добавления экземпляра TSPItem к свойству items объекта user .

1
2
NSMutableSet *items = [user mutableSetValueForKey:@»items»];
[items addObject:item];

Если вы соберете проект и запустите приложение в iOS Simulator, вы заметите, что приложение вылетает. Выходные данные в консоли XCode говорят нам, что модель данных, используемая для открытия постоянного хранилища, несовместима с той, которая использовалась для его создания. Причина этой проблемы подробно объясняется в предыдущей статье этой серии. Если вы хотите узнать больше о миграциях и о том, как безопасно изменить модель данных, я предлагаю вам прочитать эту статью .

Подклассы NSManagedObject очень распространены при работе с Core Data. Это не только повышает безопасность типов, но и значительно облегчает работу с отношениями.

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