1. Введение
Ранее в этой серии мы создали простое приложение 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
для каждой сущности модели данных, и об этом вы узнаете в этой статье.
2. Подклассы NSManagedObject
Чтобы сэкономить время, мы вернемся к Done , приложению, которое мы создали ранее в этой серии. Загрузите его с GitHub и откройте в Xcode.
Создать подкласс NSManagedObject
очень легко. Хотя можно вручную создать подкласс NSManagedObject
для сущности, проще позволить XCode выполнить всю работу за вас.
Откройте модель данных проекта Done.xcdatamodeld и выберите объект TSPItem . Выберите New> File … из меню File Xcode, выберите шаблон подкласса NSManagedObject из раздела Core Data и нажмите Next .
Установите флажок правильной модели данных, Готово , в списке моделей данных и нажмите Далее .
На следующем шаге вас попросят выбрать сущности, для которых вы хотите создать подкласс NSManagedObject
. Проверьте флажок объекта TSPItem и нажмите Далее .
Выберите место для хранения файлов классов подкласса NSManagedObject
и убедитесь, что параметр Использовать скалярные свойства для примитивных типов данных не отмечен. Я объясню значение этой опции через несколько минут. Нажмите кнопку Создать, чтобы создать подкласс NSManagedObject
для сущности TSPItem .
3. NSManagedObject
Anatomy
Интерфейс
Перейдите к файлам 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
три предупреждения, сообщающих нам, что средства доступа к этим трем свойствам отсутствуют.
4. Обновление проекта
С готовым к использованию классом TSPItem
пришло время обновить проект, заменив все вхождения valueForKey:
и setValue:forKey:
TSPViewController
Откройте файл 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
Нам также нужно внести несколько изменений в класс 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
. Давайте начнем с интерфейса класса. Откройте 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
. Тип переменной record
— NSManagedObject
, но класс 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, чтобы увидеть, все ли работает по-прежнему.
5. Отношения
Текущая модель данных не содержит никаких отношений, но давайте добавим несколько, чтобы увидеть, как выглядит подкласс 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
Давайте сначала посмотрим на изменения недавно сгенерированного класса 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
Класс TSPUser
немного сложнее. Посмотрите на интерфейс класса. Первое, что вы заметите, это то, что тип свойства items
— NSSet
. Это не должно быть сюрпризом, потому что мы уже знали, что 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];
|
6. Миграции
Если вы соберете проект и запустите приложение в iOS Simulator, вы заметите, что приложение вылетает. Выходные данные в консоли XCode говорят нам, что модель данных, используемая для открытия постоянного хранилища, несовместима с той, которая использовалась для его создания. Причина этой проблемы подробно объясняется в предыдущей статье этой серии. Если вы хотите узнать больше о миграциях и о том, как безопасно изменить модель данных, я предлагаю вам прочитать эту статью .
Вывод
Подклассы NSManagedObject
очень распространены при работе с Core Data. Это не только повышает безопасность типов, но и значительно облегчает работу с отношениями.
В следующей части этой серии мы подробнее рассмотрим основные данные и параллелизм. Параллельность — сложная концепция практически для любого языка программирования, но знание того, каких ошибок следует избегать, делает ее гораздо менее пугающей.