16.0. Введение
Core Data — это мощный фреймворк, входящий в состав iOS SDK. Он позволяет программисту сохранять данные и управлять ими объектно-ориентированным способом. Традиционно программисту приходилось сохранять данные на диске, пользуясь архивационными возможностями Objective-C, либо записывать данные в файлы, а потом управлять ими вручную. С появлением Core Data программист может просто взаимодействовать с его объектно-ориентированным интерфейсом и эффективно управлять своими данными. В этой главе будет рассмотрено, как использовать Core Data для создания модели своего приложения (с применением программной архитектуры «модель — вид — контроллер»).
Фреймворк Core Data обеспечивает низкоуровневые взаимодействия с хранилищем данных устройства, то есть взаимодействия, незаметные для программиста. iOS сама определяет, как будет организовано низкоуровневое управление данными. Для реализации такого взаимодействия программисту достаточно знать, какой высокоуровневый API для этого предназначен. Но при этом важно понимать и структуру фреймворка Core Data, его внутреннее функционирование. Чтобы лучше с этим разобраться, создадим приложение, использующее Core Data.
Теперь у нас есть новый компилятор LLVM, поэтому, чтобы включить фреймворк Core Data в ваш проект, достаточно включить обобщающий заголовок этого фреймворка, вот так:
#import «AppDelegate.h»
#import
@implementation AppDelegate
<# Остаток вашего кода находится здесь #>
Для работы с Core Data необходимо понимать стек этого фреймворка, который составляют следующие основные элементы:
• постоянное хранилище данных — объект, представляющий находящуюся на диске базу данных. Мы никогда не используем этот объект непосредственно;
координатор постоянного хранилища данных — объект, координирующий считывание информации из постоянного хранилища и запись в него. Координатор — это промежуточное звено между контекстом управляемых объектов и постоянным хранилищем данных;
модель управляемого объекта (MOM) — обычный файл на диске, который будет представлять нашу модель данных. Считайте, что это схема базы данных;
управляемый объект — этот класс представляет сущность, которую мы хотим сохранить в Core Data. В традиционном программировании баз данных такие сущности называются таблицами. Управляемый объект относится к типу NSManagedObject, экземпляры таких объектов помещаются в контекстах управляемых объектов. Они соответствуют схеме, заложенной в модели управляемого объекта, и сохраняются в постоянном хранилище данных с помощью координатора;
контекст управляемых объектов — его можно сравнить с виртуальной приборной панелью. Странно звучит, да? Сейчас все будет понятно. Мы создаем объекты Core Data в памяти, задаем их свойства и манипулируем ими. Все эти операции происходят в контексте управляемого объекта. Контекст отслеживает все операции, совершаемые над управляемыми объектами, и даже позволяет нам отменять такие действия. Представьте другую метафору: ваши управляемые объекты, находящиеся в контексте, — это игрушки, а сам контекст — это стол, на который вы их положили. Игрушки можно передвигать на столе, разбирать, убирать какие-то со стола и класть на их место новые. Итак, стол — это контекст управляемых объектов. Закончив манипуляции с объектами, вы можете сохранить его состояние. Когда мы сохраняем состояние контекста управляемых объектов, информация об операции сохранения передается в постоянное хранилище данных. Это делается посредством координатора, к которому подключен контекст. На основании этой информации координатор хранилища данных будет записывать информацию в постоянное хранилище данных, а затем — на диск.
Чтобы добавить Core Data в свой проект, а затем приступить к использованию всех его классных возможностей, просто создайте проект. Затем, когда система спросит, добавлять ли к нему Core Data, установите соответствующий флажок (рис. 16.1).
Рис. 16.1. Добавление Core Data к новому проекту Xcode
После того как вы создадите проект с Core Data, у делегата вашего приложения появится ряд новых свойств:
NSManagedObjectContext *managedObjectContext;
NSManagedObjectModel *managedObjectModel;
NSPersistentStoreCoordinator *persistentStoreCoordinator;
Вы уже понимаете, что означают эти свойства, — они были описаны ранее в данной главе. Контекст — наш игровой стол, модель — схема базы данных, а координатор — объект, опосредующий сохранение контекста на диске. Все просто. Теперь переходим к изучению основных разделов этой главы.
16.1. Создание модели Core Data с помощью Xcode
Постановка задачи
Требуется визуально спроектировать в Xcode модель данных для вашего приложения iOS.
Решение
Следуя инструкциям из введения к данной главе, создайте проект Core Data. Потом найдите в пакете вашего приложения файл с расширением xcdatamodel и откройте его в визуальном редакторе данных (рис. 16.2).
Рис. 16.2. Визуальный редактор данных в Xcode
Обсуждение
Визуальный редактор данных Xcode — потрясающий инструмент, позволяющий программисту с легкостью проектировать модель данных для своего приложения. Прежде чем приступить к работе с этим инструментом, необходимо усвоить два очень важных определения:
• сущность (Entity) — аналогична таблице базы данных;
атрибут (Attribute) — аналогичен столбцу в базе данных.
Позже сущности станут объектами (управляемыми объектами). Это произойдет после того, как мы сгенерируем код на базе нашей объектной модели. Об этом пойдет речь в разделе 16.2. В текущем разделе мы сосредоточимся на создании модели данных в визуальном редакторе.
В нижней части окна редактора найдите кнопку +. Щелкните правой кнопкой мыши, удерживая указатель на этом плюсике, а потом выберите из контекстного меню вариант Add Entity (Добавить сущность) (рис. 16.3).
Рис. 16.3. Добавление новой сущности к модели данных
Сущность, которую вы создали, сразу же после создания будет находиться в состоянии, позволяющем немедленно ее переименовать. Измените название этой сущности на Person (Контакт) (рис. 16.4).
Рис. 16.4. Изменение имени новой сущности на Person
Выберите сущность Person, потом щелкните на + в области Attributes (Атрибуты) и создайте для сущности три следующих атрибута (рис. 16.5):
• firstName (типа String);
• lastName (типа String);
• age (типа Integer 32).
Рис. 16.5. Мы добавили три атрибута к сущности Person
В редакторе модели данных выберите из меню View (Вид) в Xcode команду Utilities — Show Utilities (Вспомогательная область — Отобразить вспомогательные возможности). В правой части Xcode откроется вспомогательная область. В верхней части этой области нажмите кнопку Data Model Inspector (Инспектор модели данных) и убедитесь, что не забыли щелкнуть на только что созданной нами сущности Person (Контакт). На данном этапе инспектор модели данных заполнится элементами, относящимися к сущности Person (рис. 16.6).
Рис. 16.6. Инспектор модели данных отображается в правой части окна Xcode
Теперь щелкните на атрибутах firstName, lastName и age сущности Person. Убедитесь, что атрибуты firstName и lastName не являются опциональными — флажок Optional должен быть снят. При этом для атрибута age флажок Optional должен быть установлен.
Итак, мы создали модель. Выполните команду File — Save (Файл — Сохранить), чтобы убедиться, что сделанные изменения сохранены. О том, как сгенерировать код на базе только что созданной вами модели, рассказывается в разделе 16.2.
16.2. Генерирование файлов классов для сущностей Core Data
Постановка задачи
Вы выполнили все инструкции из раздела 16.1. Теперь требуется научиться создавать код на основании имеющейся объектной модели.
Решение
Выполните следующие шаги.
1. В Xcode найдите созданный для вашего приложения файл с расширением xcdatamodel. Он был заготовлен на этапе создания самого приложения в Xcode. Щелкните на этом файле — и вы должны увидеть, как в правой части окна Xcode открывается редактор.
2. Выберите сущность Person, созданную нами ранее (см. раздел 16.1).
3. Выполните в Xcode команду File — New File (Файл — Новый файл).
4. В диалоговом окне New File (Новый файл) убедитесь, что выбрали iOS в качестве основной категории, а Core Data — в качестве подкатегории. Потом укажите в правой части окна элемент NSManagedObject subclass (Подкласс NSManagedObject) и нажмите Next (Далее) (рис. 16.7).
Рис. 16.7. Создание в Xcode подкласса управляемого объекта
5. На следующем экране выберите модель управляемого объекта, которую вы хотите сохранить на диске, и отметьте ее флажком. Сделав это, нажмите кнопку Next (Далее) (рис. 16.8).
Рис. 16.8. Выбор модели управляемого объекта для сохранения на диске
Если в вашем проекте всего одна модель, то и в списке вы увидите всего одну модель управляемого объекта. Но на рис. 16.8 мы видим много моделей. Дело в том, что в моем рабочем пространстве в Xcode присутствует множество проектов и каждый из них имеет свою модель.
6. Теперь система предложит вам выбрать сущности, которые вы хотите экспортировать из своей модели на диск в виде файлов Objective-C. Поскольку мы создали всего одну сущность — Person, ваш список будет выглядеть примерно как на рис. 16.9. Убедитесь, что сущность Person отмечена, а потом нажмите кнопку Next (Далее).
Рис. 16.9. Экспорт сущности Person на диск в качестве управляемого объекта
7. На последнем этапе вам будет предложено сохранить сущность на диске. Убедитесь, что ваш проект отмечен в поле Targets (Цели) (рис. 16.10). В противном случае эта сущность не будет доступна в других файлах исходного кода, используемых в проекте. Если вас все устраивает, нажмите кнопку Create (Создать).
Рис. 16.10. Сохранение сущности на диске
Итак, вы увидите в своем проекте два файла, которые называются Person.h и Person.m. Откройте файл Person.h. Там будет написано следующее:
#import
#import
@interface Person: NSManagedObject
@property (nonatomic, retain) NSNumber * age;
@property (nonatomic, retain) NSString * firstName;
@property (nonatomic, retain) NSString * lastName;
@end
Файл Person.m реализуется следующим образом:
#import «Person.h»
@implementation Person
@dynamic age;
@dynamic firstName;
@dynamic lastName;
@end
Вот и все! Мы выполнили реальное определение и реализацию управляемого объекта. В разделе 16.3 мы научимся инстанцировать и сохранять управляемый объект типа Person в контексте управляемых объектов приложения.
Обсуждение
Когда мы создавали в Xcode модель данных с помощью редактора, в ходе этой работы создавали отношения данных, сущности, атрибуты и т. д. Тем не менее, чтобы эту модель можно было использовать в приложении, для нее нужно сгенерировать код. Если просмотреть файлы. h и. m ваших сущностей, то выяснится, что все атрибуты присваиваются динамически. В.m-файле сущностей вы увидите директиву @dynamic. Она сообщает компилятору, что вы выполните запрос каждого атрибута во время исполнения с применением динамического расширения метода.
Код, применяемый во фреймворке Core Data к вашим сущностям, остается совершенно невидимым. На самом деле действительно нет никакой необходимости в том, чтобы программист видел этот код. Все, о чем следует знать, — сущность Person имеет три атрибута: firstName, lastName и age. Этим атрибутам можно присваивать значения (если они являются свойствами, доступными для чтения и записи), их можно сохранять в контекст и загружать из контекста, как будет показано в разделе 16.3.
16.3. Создание и сохранение данных с помощью Core Data
Постановка задачи
Вы создали управляемый объект. После этого вы хотите инстанцировать его и вставить этот экземпляр в контекст Core Data вашего приложения.
Решение
Выполните инструкции, описанные в разделах 16.1 и 16.2. Теперь можно использовать метод класса insertNewObjectForEntityForName: inManagedObjectContext:, относящийся к классу NSEntityDescription, чтобы создать новый объект типа, указанного в первом параметре этого метода. Как только будет создана новая сущность (управляемый объект), ее можно будет изменить, модифицируя ее свойства. После того как все будет готово, сохраните контекст управляемого объекта с помощью метода экземпляра save:, относящегося к контексту управляемых объектов.
Предполагается, что вы уже создали в Xcode универсальное приложение под названием Creating and Saving Data Using Core Data. Теперь, чтобы вставить новый управляемый объект в контекст, выполните следующие шаги.
1. Найдите файл под названием Creating_and_Saving_Data_Using_Core_DataAppDelegate.m.
2. Импортируйте файл Person.h в файл реализации делегата приложения:
Сущность Person мы создали в разделе 16.1.
#import «AppDelegate.h»
#import «Person.h»
@implementation AppDelegate
@synthesize managedObjectContext = _managedObjectContext;
@synthesize managedObjectModel = _managedObjectModel;
@synthesize persistentStoreCoordinator = _persistentStoreCoordinator;
<# Остаток кода вашего приложения находится здесь #>
В методе application: didFinishLaunchingWithOptions: совместно используемого делегата приложения напишем следующий код:
— (BOOL) application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions{
Person *newPerson = [NSEntityDescription
insertNewObjectForEntityForName:@"Person"
inManagedObjectContext: self.managedObjectContext];
if (newPerson!= nil){
newPerson.firstName = @"Anthony";
newPerson.lastName = @"Robbins";
newPerson.age = @51;
NSError *savingError = nil;
if ([self.managedObjectContext save:&savingError]){
NSLog(@"Successfully saved the context.");
} else {
NSLog(@"Failed to save the context. Error = %@", savingError);
}
} else {
NSLog(@"Failed to create the new person.");
}
self.window = [[UIWindow alloc] initWithFrame:
[[UIScreen mainScreen] bounds]];
self.window.backgroundColor = [UIColor whiteColor];
[self.window makeKeyAndVisible];
return YES;
}
Обсуждение
В предыдущих разделах было показано, как с помощью редактора Xcode создавать сущности и генерировать на их основе код. Далее нужно приступить к использованию этих сущностей и инстанцировать их. Для этого мы используем класс NSEntityDescription и вызываем метод insertNewObjectForEntityForName: inManagedObjectContext: этого класса. В таком случае будет производиться поиск заданной сущности (указанной с именем NSString) в обозначенном контексте управляемых объектов. Ситуация напоминает процесс создания новой строки (управляемый объект) в таблице (сущность) базы данных (контекст управляемых объектов).
При попытке вставить в контекст управляемых объектов неизвестную сущность возникнет исключение типа NSInternalInconsistencyException.
После того как в контекст будет вставлена новая сущность, его необходимо сохранить. В результате все несохраненные данные контекста будут сброшены в долговременную память. Это можно сделать с помощью метода экземпляра save:, относящегося к нашему контексту управляемых объектов. Если логическое (BOOL) возвращаемое значение этого метода равно YES, мы можем быть уверены, что контекст сохранен. В разделе 16.4 будет рассмотрено, как считывать данные назад в оперативную память.
16.4. Считывание данных из Core Data
Постановка задачи
Требуется считывать содержимое ваших сущностей (таблиц) с помощью Core Data.
Решение
Воспользуйтесь экземпляром класса NSFetchRequest:
— (BOOL) createNewPersonWithFirstName:(NSString *)paramFirstName
lastName:(NSString *)paramLastName
age:(NSUInteger)paramAge{
BOOL result = NO;
if ([paramFirstName length] == 0 ||
[paramLastName length] == 0){
NSLog(@"First and Last names are mandatory.");
return NO;
}
Person *newPerson = [NSEntityDescription
insertNewObjectForEntityForName:@"Person"
inManagedObjectContext: self.managedObjectContext];
if (newPerson == nil){
NSLog(@"Failed to create the new person.");
return NO;
}
newPerson.firstName = paramFirstName;
newPerson.lastName = paramLastName;
newPerson.age = @(paramAge);
NSError *savingError = nil;
if ([self.managedObjectContext save:&savingError]){
return YES;
} else {
NSLog(@"Failed to save the new person. Error = %@", savingError);
}
return result;
}
— (BOOL) application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions{
[self createNewPersonWithFirstName:@"Anthony"
lastName:@"Robbins"
age:51];
[self createNewPersonWithFirstName:@"Richard"
lastName:@"Branson"
age:61];
/* Сообщаем запросу, что мы собираемся считать содержимое
сущности Person */
/* Сначала создаем запрос выборки данных. */
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc]
initWithEntityName:@"Person"];
NSError *requestError = nil;
/* И применяем к контексту запрос выборки данных. */
NSArray *persons =
[self.managedObjectContext executeFetchRequest: fetchRequest
error:&requestError];
/* Убеждаемся, что получили массив. */
if ([persons count] > 0){
/* По порядку перебираем все контакты, содержащиеся в массиве. */
NSUInteger counter = 1;
for (Person *thisPerson in persons){
NSLog(@"Person %lu First Name = %@",
(unsigned long)counter,
thisPerson.firstName);
NSLog(@"Person %lu Last Name = %@",
(unsigned long)counter,
thisPerson.lastName);
NSLog(@"Person %lu Age = %ld",
(unsigned long)counter,
(unsigned long)[thisPerson.age unsignedIntegerValue]);
counter++;
}
} else {
NSLog(@"Could not find any Person entities in the context.");
}
self.window = [[UIWindow alloc] initWithFrame:
[[UIScreen mainScreen] bounds]];
self.window.backgroundColor = [UIColor whiteColor];
[self.window makeKeyAndVisible];
return YES;
}
В данном коде мы используем переменную счетчика внутри блока быстрого перебора. Причина, по которой в ходе этого быстрого перебора требуется счетчик, заключается в том, что на консоль выводятся отладочные сообщения NSLog, которые мы просматриваем, чтобы узнать для перечисляемого в данный момент объекта его индекс в массиве. Альтернативным вариантом решения было бы использование классического for-цикла с переменной счетчика.
Подробнее о запросах выборки данных поговорим в подразделе «Обсуждение» данного раздела.
Обсуждение
Тем, кто знаком с терминологией баз данных, запрос выборки данных напомнит оператор SELECT. В операторе SELECT мы указываем, какие строки и при каких условиях должны быть возвращены из какой таблицы. Запрос выборки данных делает то же самое. Мы указываем сущность (таблицу) и контекст управляемых объектов (уровень базы данных). Кроме того, можем задавать дескрипторы сортировки для данных, которые мы считываем. Но сначала поговорим о том, как упростить сам процесс считывания данных.
Чтобы считать сущность Person (эту сущность мы создали в разделе 16.1 и превратили ее в код в разделе 16.2), мы сначала приказываем классу NSEntityDescription произвести в нашем контексте управляемых объектов поиск сущности под названием Person. Как только она будет найдена, сообщим запросу выборки данных, что требуется считать информацию из этой сущности. После этого нам останется всего лишь выполнить запрос выборки данных, как было показано в подразделе «Решение» данного раздела.
Метод экземпляра executeFetchRequest: error:, относящийся к классу NSManagedObjectContext, может иметь в качестве возвращаемого значения либо nil (в случае ошибки), либо массив управляемых объектов Person. Если по заданной сущности не найдено никаких результатов, то возвращенный массив будет пустым.
См. также
Разделы 16.1 и 16.2.
16.5. Удаление данных из Core Data
Постановка задачи
Требуется удалить управляемый объект (строку таблицы) из контекста управляемых объектов (вашей базы данных).
Решение
Воспользуйтесь методом экземпляра deleteObject:, относящимся к классу NSManagedObjectContext:
— (BOOL) application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions{
[self createNewPersonWithFirstName:@"Anthony"
lastName:@"Robbins"
age:51];
[self createNewPersonWithFirstName:@"Richard"
lastName:@"Branson"
age:61];
/* Сначала создаем запрос выборки данных. */
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSError *requestError = nil;
/* Теперь применим запрос выборки данных к контексту. */
NSArray *persons =
[self.managedObjectContext executeFetchRequest: fetchRequest
error:&requestError];
/* Убеждаемся, что получили массив. */
if ([persons count] > 0){
/* Удаляем последний контакт из массива. */
Person *lastPerson = [persons lastObject];
[self.managedObjectContext deleteObject: lastPerson];
NSError *savingError = nil;
if ([self.managedObjectContext save:&savingError]){
NSLog(@"Successfully deleted the last person in the array.");
} else {
NSLog(@"Failed to delete the last person in the array.");
}
} else {
NSLog(@"Could not find any Person entities in the context.");
}
self.window = [[UIWindow alloc] initWithFrame:
[[UIScreen mainScreen] bounds]];
self.window.backgroundColor = [UIColor whiteColor];
[self.window makeKeyAndVisible];
return YES;
}
В приведенном примере кода используется метод createNewPersonWithFirstName: lastName: age:, который мы написали в разделе 16.4.
Обсуждение
Можно удалять управляемые объекты (записи из таблицы базы данных) с помощью метода экземпляра deleteObject:, относящегося к классу NSManagedObjectContext.
Ни в одном из своих параметров этот метод не сообщает вам об ошибке, равно как и не возвращает значения BOOL. Таким образом, у вас нет надежного способа узнать, был ли объект успешно удален с помощью контекста управляемых объектов. Для получения этой информации лучше использовать метод isDeleted управляемого объекта.
С учетом данной информации изменим код, написанный ранее в этом разделе:
— (BOOL) application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions{
[self createNewPersonWithFirstName:@"Anthony"
lastName:@"Robbins"
age:51];
[self createNewPersonWithFirstName:@"Richard"
lastName:@"Branson"
age:61];
/* Сначала создаем запрос выборки данных. */
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSError *requestError = nil;
/* Теперь применим запрос выборки данных к контексту. */
NSArray *persons =
[self.managedObjectContext executeFetchRequest: fetchRequest
error:&requestError];
/* Убеждаемся, что получили массив. */
if ([persons count] > 0){
/* Удаляем последний контакт из массива. */
Person *lastPerson = [persons lastObject];
[self.managedObjectContext deleteObject: lastPerson];
if ([lastPerson isDeleted]){
NSLog(@"Successfully deleted the last person…");
NSError *savingError = nil;
if ([self.managedObjectContext save:&savingError]){
NSLog(@"Successfully saved the context.");
} else {
NSLog(@"Failed to save the context.");
}
} else {
NSLog(@"Failed to delete the last person.");
}
} else {
NSLog(@"Could not find any Person entities in the context.");
}
self.window = [[UIWindow alloc] initWithFrame:
[[UIScreen mainScreen] bounds]];
self.window.backgroundColor = [UIColor whiteColor];
[self.window makeKeyAndVisible];
return YES;
}
После запуска приложения в окне консоли отобразится примерно следующий результат:
Successfully deleted the last person… // последний контакт успешно удален
Successfully saved the context. // контекст успешно сохранен
16.6. Сортировка данных в Core Data
Постановка задачи
Требуется сортировать управляемые объекты (записи), выбираемые из контекста управляемых объектов (базы данных).
Решение
Нужно создать по экземпляру класса NSSortDescriptor для каждого атрибута (в терминологии баз данных — столбца) той сущности, в которой требуется произвести сортировку. Дескрипторы сортировки добавляются к массиву, а сам массив присваивается экземпляру NSFetchRequest с помощью метода экземпляра setSortDescriptors:. В данном коде, приведенном в качестве примера, Sorting_Data_in_Core_DataAppDelegate — это класс, представляющий делегат универсального приложения (о том, как создается сущность Person, рассказано в разделах 16.1 и 16.2:
— (BOOL) application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions{
[self createNewPersonWithFirstName:@"Richard"
lastName:@"Branson"
age:61];
[self createNewPersonWithFirstName:@"Anthony"
lastName:@"Robbins"
age:51];
/* Сначала создаем запрос выборки данных. */
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc]
initWithEntityName:@"Person"];
NSSortDescriptor *ageSort =
[[NSSortDescriptor alloc] initWithKey:@"age"
ascending: YES];
NSSortDescriptor *firstNameSort =
[[NSSortDescriptor alloc] initWithKey:@"firstName"
ascending: YES];
fetchRequest.sortDescriptors = sortDescriptors;
/* Сообщаем запросу, что сначала мы хотим
считать содержимое сущности Person. */
[fetchRequest setEntity: entity];
NSError *requestError = nil;
/* Теперь применим запрос выборки данных к контексту. */
NSArray *persons =
[self.managedObjectContext executeFetchRequest: fetchRequest
error:&requestError];
for (Person *person in persons){
NSLog(@"First Name = %@", person.firstName);
NSLog(@"Last Name = %@", person.lastName);
NSLog(@"Age = %lu", (unsigned long)[person.age unsignedIntegerValue]);
}
self.window = [[UIWindow alloc] initWithFrame:
[[UIScreen mainScreen] bounds]];
self.window.backgroundColor = [UIColor whiteColor];
[self.window makeKeyAndVisible];
return YES;
}
Обсуждение
Экземпляр класса NSFetchRequest может нести с собой массив экземпляров NSSortDescriptor. Каждый дескриптор сортировки определяет атрибут (столбец) актуальной сущности, в которой необходимо произвести сортировку. Кроме того, он указывает порядок сортировки — восходящий или нисходящий. Например, сущность Person, которую мы создали в разделе 16.1, имеет атрибуты firstName, lastName и age. Если мы хотим считать все контакты в контексте управляемых объектов и отсортировать этих людей по возрасту, от самого младшего до самого старшего, то создадим экземпляр NSSortDescriptor с ключом age и зададим для него восходящий порядок (ascending):
NSSortDescriptor *ageSortDescriptor =
[[NSSortDescriptor alloc] initWithKey:@"age"
ascending: YES];
Запросу выборки данных можно присвоить более одного дескриптора сортировки. Порядок расположения данных в массиве определяет и порядок, в котором задаются дескрипторы. Иными словами, вывод сортируется по первому дескриптору в массиве, в полученном множестве записи сортируются по второму дескриптору в массиве и т. д.
См. также
Раздел 16.4.
16.7. Оптимизация доступа к данным в табличных видах
Постановка задачи
Имеется приложение, в котором пользователь просматривает управляемые объекты в табличных видах. В этом приложении вы хотите выбирать и представлять данные более гибким и естественным образом, не управляя ими при этом вручную.
Решение
Воспользуйтесь контроллерами для представления результатов выборки, которые являются экземплярами класса NSFetchedResultsController.
В этом разделе для ускорения разработки рассматриваемого приложения будут применены раскадровки. Подробнее о раскадровках рассказано в главе 6.
Обсуждение
Контроллер для представления результатов выборки (Fetched Result Controller) функционально аналогичен табличному виду. Как и в таблице, в нем есть разделы и строки. Контроллер для представления результатов выборки может считывать управляемые объекты из соответствующего контекста, а также подразделять эти объекты на разделы и строки. Каждый раздел является группой, если задать такое условие в параметрах запроса, а каждая строка в разделе является управляемым объектом. Есть несколько важных причин, по которым может понадобиться модифицировать ваше приложение, чтобы в нем можно было применять контроллеры для представления результатов выборки. Эти причины таковы.
• После создания контроллера для представления результатов выборки в контексте управляемых объектов любое изменение данных (вставка, удаление, модификация и т. д.) немедленно отразится и в контроллере для представления результатов выборки. Например, можно создать контроллер для представления результатов выборки, чтобы считывать управляемые объекты сущности Person. Потом где-то в другой точке вашего приложения может понадобиться вставить в контекст новый управляемый объект Person (речь идет о том самом контексте, в котором был создан контроллер для представления результатов выборки). Сразу же после этого новый управляемый объект станет доступен в контроллере для представления результатов выборки. Чудеса, да и только!
• Имея контроллер для представления результатов выборки, можно более эффективно управлять кэшем. Например, можно указать контроллеру для представления результатов выборки сохранить в памяти только N управляемых объектов на каждый экземпляр такого контроллера.
• Контроллеры для представления результатов выборки аналогичны табличным видам в том отношении, что в них, как и в таблицах, есть разделы и строки — об этом говорилось ранее. Можно использовать контроллер для представления результатов выборки, чтобы без труда отображать табличные виды вашего приложения прямо в графическом пользовательском интерфейсе.
Рассмотрим некоторые важные свойства и методы экземпляра, относящиеся к контроллерам для представления результатов выборки (все объекты относятся к типу NSFetchedResultsController).
• sections (свойство типа NSArray) — контроллер для представления результатов выборки может группировать данные, используя путь к ключу. Выделенный инициализатор класса NSFetchedResultsController принимает данный группирующий фильтр в параметре sectionNameKeyPath. После этого в массиве sections будут содержаться все сгруппированные разделы. Каждый объект данного массива соответствует протоколу NSFetchedResultsSectionInfo.
• objectAtIndexPath: (метод экземпляра, возвращает управляемый объект) — объекты, выбираемые с помощью описываемого контроллера, их можно получать по индексу в разделе или строке. Строки каждого раздела нумеруются от 0 до N — 1, где N — общее количество элементов в данном разделе. В объекте пути к индексу указывается как индекс раздела, так и индекс строки, и в результате этого совершенно точно формулируется информация, необходимая для получения конкретных объектов от контроллера для представления результатов выборки. Метод экземпляра objectAtIndexPath принимает индексные пути. Каждый индексный путь — это объект типа NSIndexPath. Если требуется создать ячейку табличного вида, воспользовавшись управляемым объектом из контроллера для представления результатов выборки, то нужно просто передать объект индексного пути методу делегата табличного вида tableView: cellForRowAtIndexPath:. Такая передача происходит в параметре cellForRowAtIndexPath этого метода. Если вы хотите сами создать индексный путь в любой другой точке вашего приложения, пользуйтесь методом класса indexPathForRow: inSection:, относящимся к классу NSIndexPath.
• fetchRequest (свойство типа NSFetchRequest) — если в любой точке вашего приложения возникнет необходимость заменить объект запроса выборки контроллером для представления результатов выборки, то это можно сделать с помощью свойства fetchRequest экземпляра NSFetchedResultsController. Такая возможность будет полезна, например, если необходимо изменить дескрипторы сортировки (о них подробно рассказано в разделе 16.6) запроса выборки — уже после того, как вы выделили и инициализировали ваши контроллеры для представления результатов выборки.
Контроллер для представления результатов выборки также отслеживает изменения, происходящие в том контексте, с которым он связан. Допустим, контроллер для представления результатов выборки создан в контроллере вида А, а в контроллере вида B мы удаляем объект из нашего контекста. Поскольку удаление происходит в контроллере вида B, первый контроллер вида А, владеющий контроллером для представления результатов выборки, будет об этом уведомлен. При этом предполагается, что контроллер вида А является делегатом контроллера для представления результатов выборки. Такое соотношение контроллеров удобно и очень нам пригодится. Предположим следующее: мы разрабатываем приложение, в котором пользователь видит на экране два контроллера вида. Корневой контроллер вида является табличным. В нем перечислены все пользовательские контакты. Во втором контроллере вида пользователь может добавить новый контакт. Как только пользователь нажмет в контроллере вида кнопку Save (Сохранить) и вернется к списку своих контактов, этот список уже будет обновлен благодаря механизму делегирования, действующему в контроллере, представляющем результаты выборки.
В описанном приложении потребуется объявить табличный контроллер вида, в котором все пользовательские контакты перечислены следующим образом:
#import «PersonsListTableViewController.h»
#import «AppDelegate.h»
#import «Person.h»
#import «AddPersonViewController.h»
static NSString *PersonTableViewCell = @"PersonTableViewCell";
@interface PersonsListTableViewController ()
@property (nonatomic, strong) UIBarButtonItem *barButtonAddPerson;
@property (nonatomic, strong) NSFetchedResultsController *frc;
@end
Кнопка панели, объявленная в этом коде, будет представлять собой простую кнопку +. Этот плюсик будет находиться на навигационной панели. Такая кнопка позволяет пользователю перейти в контроллер вида Add Person (Добавить контакт), где можно будет добавить новый контакт в имеющийся контекст управляемых объектов. Контроллер для представления результатов выборки также будет использоваться для выборки контактов из контекста и последующего их отображения в табличном виде.
Вот как создается контроллер для представления результатов выборки:
/* Сначала создаем запрос выборки данных */
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc]
initWithEntityName:@"Person"];
NSSortDescriptor *ageSort =
[[NSSortDescriptor alloc] initWithKey:@"age"
ascending: YES];
NSSortDescriptor *firstNameSort =
[[NSSortDescriptor alloc] initWithKey:@"firstName"
ascending: YES];
fetchRequest.sortDescriptors = @[ageSort, firstNameSort];
self.frc =
[[NSFetchedResultsController alloc]
initWithFetchRequest: fetchRequest
managedObjectContext: [self managedObjectContext]
sectionNameKeyPath: nil
cacheName: nil];
self.frc.delegate = self;
NSError *fetchingError = nil;
if ([self.frc performFetch:&fetchingError]){
NSLog(@"Successfully fetched.");
} else {
NSLog(@"Failed to fetch.");
}
Как видите, контроллер для представления результатов выборки принимает контроллер актуального табличного вида в качестве своего делегата. Делегат контроллера для представления результатов выборки должен соответствовать протоколу NSFetchedResultsControllerDelegate. Вот некоторые из наиболее важных методов этого протокола.
• controllerWillChangeContent: — вызывается в делегате и сообщает ему об изменении контекста, служащего основой для контроллера, представляющего результаты выборки, а также о том, что содержимое контроллера, представляющего результаты выборки, вот-вот изменится с учетом внесенных изменений. Обычно этот метод используется для подготовки табличного вида к изменениям. Для этого в нем вызывается метод beginUpdates.
• controller: didChangeObject: atIndexPath: forChangeType: newIndexPath: — вызывается в делегате и сообщает ему о конкретных изменениях, сделанных в объекте из контекста. Например, если вы удаляете объект в контексте, то вызывается этот метод. При этом его параметр forChangeType содержит значение NSFetchedResultsChangeDelete. В другом случае, когда вы вставляете новый объект в контекст, этот параметр содержит значение NSFetchedResultsChangeInsert.
Кроме того, этот метод вызывается в методе делегата контроллера для представления результатов выборки, когда обновляется управляемый объект. Это происходит после того, как объект будет сохранен в объекте с помощью метода save:.
• controllerDidChangeContent: — вызывается в делегате и информирует его о том, что контроллер для представления результатов выборки был обновлен в результате обновления контекста управляемых объектов. Как правило, именно внутри этого метода программисты совершают вызов endUpdates, применяемый в табличных видах для обработки всех обновлений, поступивших в таблицу после срабатывания метода beginUpdates.
Вот типичная реализация вышеупомянутых методов в приложении, которое было описано ранее в этом разделе:
— (void) controllerWillChangeContent:(NSFetchedResultsController *)controller{
[self.tableView beginUpdates];
}
— (void) controller:(NSFetchedResultsController *)controller
didChangeObject:(id)anObject
atIndexPath:(NSIndexPath *)indexPath
forChangeType:(NSFetchedResultsChangeType)type
newIndexPath:(NSIndexPath *)newIndexPath{
if (type == NSFetchedResultsChangeDelete){
[self.tableView
deleteRowsAtIndexPaths:@[indexPath]
withRowAnimation: UITableViewRowAnimationAutomatic];
}
else if (type == NSFetchedResultsChangeInsert){
[self.tableView
insertRowsAtIndexPaths:@[newIndexPath]
withRowAnimation: UITableViewRowAnimationAutomatic];
}
}
— (void) controllerDidChangeContent:(NSFetchedResultsController *)controller{
[self.tableView endUpdates];
}
Остановимся также на передаче информации в табличный вид с помощью различных методов контроллера для представления результатов выборки — об этом мы также упоминали ранее. Одним из таких методов является objectAtIndexPath:. Простая реализация этого метода в табличном виде может выглядеть примерно так:
— (NSInteger)tableView:(UITableView *)tableView
numberOfRowsInSection:(NSInteger)section{
id
self.frc.sections[section];
return sectionInfo.numberOfObjects;
}
— (UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath{
UITableViewCell *cell = nil;
cell = [tableView dequeueReusableCellWithIdentifier: PersonTableViewCell
forIndexPath: indexPath];
Person *person = [self.frc objectAtIndexPath: indexPath];
cell.textLabel.text =
[person.firstName stringByAppendingFormat:@" %@", person.lastName];
cell.detailTextLabel.text =
[NSString stringWithFormat:@"Age: %lu",
(unsigned long)[person.age unsignedIntegerValue]];
return cell;
}
В этом коде мы приказываем нашему контроллеру табличного вида отобразить столько ячеек, сколько экземпляров управляемых объектов находится в контроллере для представления результатов выборки. Отображая каждую ячейку, мы получаем управляемый объект Person из контроллера, представляющего результаты выборки, после чего соответствующим образом конфигурируем ячейку. Контроллер табличного вида, не содержащий никаких элементов в контексте управляемых объектов, будет выглядеть примерно как на рис. 16.11.
Рис. 16.11. Пустой табличный вид, построенный на базе контроллера для представления результатов выборки
Переходим ко второму контроллеру вида, где пользователь может добавить новый экземпляр Person в контекст управляемых объектов. Воспользуемся следующим методом:
— (void) createNewPerson:(id)paramSender{
AppDelegate *appDelegate = [[UIApplication sharedApplication] delegate];
NSManagedObjectContext *managedObjectContext =
appDelegate.managedObjectContext;
Person *newPerson =
[NSEntityDescription insertNewObjectForEntityForName:@"Person"
inManagedObjectContext: managedObjectContext];
if (newPerson!= nil){
newPerson.firstName = self.textFieldFirstName.text;
newPerson.lastName = self.textFieldLastName.text;
newPerson.age = @([self.textFieldAge.text integerValue]);
NSError *savingError = nil;
if ([managedObjectContext save:&savingError]){
[self.navigationController popViewControllerAnimated: YES];
} else {
NSLog(@"Failed to save the managed object context.");
}
} else {
NSLog(@"Failed to create the new person object.");
}
}
Этот метод считывает имя, фамилию и возраст человека. На основе этих трех информационных фрагментов в контроллере вида будет создаваться контакт. Нам не придется заниматься реализацией этих текстовых полей, поскольку такая работа никак не связана с темой данного раздела. После вызова метода мы вызываем в контексте управляемого объекта метод save:. Он, в свою очередь, инициирует изменения в контроллере вида для представления результатов выборки (он находится в табличном виде). В результате всего этого табличный вид обновится.
Наконец, мы должны предоставить пользователю возможность удалять элементы в контроллере первого (табличного) вида:
— (void) tableView:(UITableView *)tableView
commitEditingStyle:(UITableViewCellEditingStyle)editingStyle
forRowAtIndexPath:(NSIndexPath *)indexPath{
Person *personToDelete = [self.frc objectAtIndexPath: indexPath];
[[self managedObjectContext] deleteObject: personToDelete];
if ([personToDelete isDeleted]){
NSError *savingError = nil;
if ([[self managedObjectContext] save:&savingError]){
NSLog(@"Successfully deleted the object");
} else {
NSLog(@"Failed to save the context with error = %@", savingError);
}
}
}
Этот код даже не затрагивает непосредственно сам контроллер для представления результатов выборки, но удаляет выбранный контакт из контекста управляемых объектов. В результате обновляется содержимое контроллера, представляющего результаты выборки, а это, в свою очередь, приводит к обновлению табличного вида. Подробнее о табличных видах рассказано в главе 4. Интерфейс нашего контроллера табличного вида в режиме удаления может выглядеть, примерно как на рис. 16.12.
Рис. 16.12. Табличный контроллер вида в режиме удаления, также здесь используется контроллер вида для представления результатов выборки
16.8. Реализация отношений в Core Data
Постановка задачи
Необходимо иметь возможность связывать управляемые объекты друг с другом, например связать контакт Person с каталогом Home, в котором он находится.
Решение
Применяйте в редакторе модели обратные отношения.
Обсуждение
В Core Data могут существовать следующие виды отношений: «один к одному» (one-to-one), обратное отношение «один ко многим» или обратное отношение «многие ко многим». Далее приведены жизненные примеры каждой разновидности отношений.
• Отношение «один к одному» — существует между человеком и его носом. У каждого человека может быть только один нос, и каждый нос может принадлежать только одному человеку.
Обратное отношение «один ко многим» — существует между сотрудником и его менеджером. У сотрудника может быть только один непосредственный менеджер, но одному менеджеру могут одновременно подчиняться несколько сотрудников. В данном случае для сотрудника создается отношение «один к одному», однако для менеджера это отношение «один (менеджер) ко многим (сотрудникам)». Поэтому такое отношение и называется обратным.
Обратное отношение «многие ко многим» — возникает между человеком и автомобилем. Одна машина может использоваться несколькими людьми, а один человек может пользоваться несколькими машинами.
В Core Data можно создавать отношения «один к одному», но я категорически не рекомендую этого делать. Возвращаясь к недавнему примеру с носом, необходимо отметить, что человек будет знать, чей нос торчит у него на лице, а вот нос не будет знать, кому он принадлежит. Обратите внимание на то, что эта система отношений «один к одному» отличается от взаимно однозначных отношений, с которыми вы могли столкнуться в других системах управления базами данных: объект A и объект B будут взаимосвязаны друг с другом, если между ними существует отношение «один к одному». В Core Data при отношении «один к одному» объект A будет знать, что связан с объектом B, но не наоборот. В объектно-ориентированном языке, таком как Objective-C, всегда лучше создавать обратные отношения, такие, которые позволяют дочерним элементам обращаться к родительским. При отношении «один ко многим» объект, который может быть ассоциирован с рядом других объектов, будет удерживать это множество объектов. Это будет множество типа NSSet. Хотя при отношениях «один к одному» оба объекта, состоящие в таких отношениях, сохраняют ссылку друг на друга, так как используют правильное имя класса «напарника», это отношение все равно принадлежит к типу «один к одному», и один объект может быть представлен в другом путем простого указания своего имени класса.
Итак, создадим такую модель данных, в которой используются преимущества обратного отношения «один ко многим».
1. Найдите в Xcode файл xcdatamodel, созданный системой в самом начале работы с проектом Core Data. Это было показано во введении к данной главе (создание такого проекта описано в разделе 16.1).
2. Откройте в редакторе файл модели данных, щелкнув на нем кнопкой мыши.
3. Удалите все созданные ранее сущности, выделяя их и нажимая клавишу Delete.
4. Создайте новую сущность и назовите ее Employee (Сотрудник). Создайте для этой сущности три атрибута, которые будут называться firstName (типа String), lastName (типа String) и age (типа Integer 32) (рис. 16.13).
Рис. 16.13. Сущность Employee с тремя атрибутами
5. Создайте сущность под названием Manager (Менеджер) с такими же атрибутами, как и у сущности Employee: firstName (типа String), lastName (типа String) и age (типа Integer 32) (рис. 16.14).
Рис. 16.14. Сущность Manager с тремя атрибутами
6. Создайте новое отношение для сущности Manager. Для этого сначала нужно выбрать данную сущность из списка, а потом нажать кнопку + в нижней части области Relationships (Отношения) (рис. 16.15).
Рис. 16.15. Добавление нового отношения к сущности Manager
7. В качестве имени нового отношения задайте employees (Сотрудники) (рис. 16.16).
Рис. 16.16. Изменение имени нового отношения типа «менеджер к сотрудникам»
8. Выберите сущность Employee и создайте для нее новое отношение. Назовите это отношение manager (рис. 16.17).
Рис. 16.17. Изменение имени нового отношения между сотрудниками и менеджером
9. Выберите сущность Manager, а потом выделите отношение employees для Manager. В области Relationships (Отношения) выберите параметр Employee (Сотрудник) в раскрывающемся меню Destination (Назначение). Именно так — ведь в этом отношении мы хотим соединить сущности Manager и Employee. В столбце Inverse (Обратные отношения) укажите значение manager (так как отношение manager будет связывать сотрудника (Employee) с менеджером (Manager)). Наконец, установите флажок To-Many Relationship (Отношение ко многим) в инспекторе модели данных (см. раздел 16.1). Результаты приведены на рис. 16.18.
Рис. 16.18. Обратное отношение, установленное между менеджером и сотрудниками
10. Выделите обе сущности (Employee и Manager), выполните команду File — New File (Файл — Новый файл) и создайте классы управляемых объектов для вашей модели, как описано в разделе 16.2.
Создав обратное отношение «один ко многим», откройте. h-файл вашей сущности Employee:
#import
#import
@class Manager;
@interface Employee: NSManagedObject
@property (nonatomic, retain) NSNumber * age;
@property (nonatomic, retain) NSString * firstName;
@property (nonatomic, retain) NSString * lastName;
@property (nonatomic, retain) Manager *manager;
@end
Как видите, в этом файле появилось новое свойство. Оно называется manager и относится к типу Manager. Таким образом, начиная с данного момента мы при наличии ссылки на конкретный объект типа Employee можем получить доступ к свойству manager, а через него — к объекту Manager данного конкретного сотрудника (если менеджер есть). Рассмотрим. h-файл сущности Manager:
#import
#import
@class Employee;
@interface Manager: NSManagedObject
@property (nonatomic, retain) NSNumber * age;
@property (nonatomic, retain) NSString * firstName;
@property (nonatomic, retain) NSString * lastName;
@property (nonatomic, retain) NSSet *employees;
@end
@interface Manager (CoreDataGeneratedAccessors)
— (void)addFKManagerToEmployeesObject:(Employee *)value;
— (void)removeFKManagerToEmployeesObject:(Employee *)value;
— (void)addFKManagerToEmployees:(NSSet *)values;
— (void)removeFKManagerToEmployees:(NSSet *)values;
@end
Для сущности Manager также создается свойство employees. Тип данных этого объекта — NSSet. Это означает, что свойство employees любого экземпляра сущности Manager может содержать от 1 до N сущностей Employee. В этом и заключается принцип отношения «один ко многим»: один менеджер, несколько сотрудников.
Другой тип отношений, которые, возможно, потребуется реализовать, называется «многие ко многим». По сравнению с отношением Manager к Employee при отношении «многие ко многим» один менеджер может иметь N сотрудников, а каждый сотрудник может подчиняться N менеджерам. Чтобы организовать такие отношения, выполните те же инструкции, что и при создании отношения «один ко многим», но выделите сущность Employee, а потом отношение manager. Измените это название на managers и установите флажок To-Many Relationship (Отношение ко многим) (рис. 16.19). Теперь стрелка будет заострена с обоих концов.
Рис. 16.19. Создание отношения «многие ко многим» между сущностями Manager и Employee
Теперь, открыв файл Employee.h, вы увидите, что его содержимое изменилось:
#import
#import
@class Manager;
@interface Employee: NSManagedObject
@property (nonatomic, retain) NSNumber * age;
@property (nonatomic, retain) NSString * firstName;
@property (nonatomic, retain) NSString * lastName;
@property (nonatomic, retain) NSSet *managers;
@end
@interface Employee (CoreDataGeneratedAccessors)
— (void)addManagersObject:(Manager *)value;
— (void)removeManagersObject:(Manager *)value;
— (void)addManagers:(NSSet *)values;
— (void)removeManagers:(NSSet *)values;
@end
Как видите, свойство managers сущности Person теперь представляет собой множество. Поскольку отношение сотрудника к его менеджерам — это множество и такое же отношение существует между менеджером и сотрудниками, здесь мы имеем пример отношения «многие ко многим»
В коде, написанном для отношения «один ко многим», можно просто создать новый управляемый объект Manager (о том, как вставлять объекты в контекст управляемых объектов, рассказано в разделе 16.3), сохранить его в контексте управляемых объектов, а потом соединить с парой управляемых объектов Employee — и их тоже сохранить в контексте. Теперь, чтобы ассоциировать менеджера с сотрудником, задайте в качестве значения для свойства FKEmployeeToManager, относящегося к экземпляру Employee, экземпляр управляемого объекта Manager. После этого фреймворк Core Data сам создаст необходимое отношение.
Если потребуется получить всех сотрудников (типа Employee), ассоциированных с объектом-менеджером (типа Manager), нужно будет просто воспользоваться методом экземпляра allObjects, относящимся к свойству FKManagerToEmployees вашего объекта-менеджера. Это объект типа NSSet, поэтому можно применить метод экземпляра allObjects, чтобы получить массив всех объектов-сотрудников, ассоциированных с конкретным объектом-менеджером.
16.9. Выборка данных в фоновом режиме
Постановка задачи
Требуется выполнять операции выборки данных в стеке Core Data, причем только в фоновом режиме. Это отличная возможность создать по-настоящему отзывчивый пользовательский интерфейс.
Решение
Перед тем как заниматься выборкой данных в фоновом режиме, создайте новый контекст управляемых объектов с параллелизмом типа NSPrivateQueueConcurrencyType. Затем воспользуйтесь методом performBlock: нового фонового контекста для выборки данных в фоновом режиме. Как только это будет сделано и вы будете готовы использовать выбранные объекты в пользовательском интерфейсе, вернитесь в поток пользовательского интерфейса с помощью dispatch_async (см. раздел 7.4). Далее для каждого объекта, выбранного в фоновом режиме, выполните в основном контексте метод objectWithID:. Так объекты, выбранные в фоновом режиме, будут перенесены в ваш приоритетный контекст, где вы сможете оперировать ими в потоке пользовательского интерфейса.
Обсуждение
Выборка объектов в основном потоке — не самая хорошая идея. Выполнять ее в главном потоке можно лишь в случаях, когда в стеке Core Data совсем немного элементов. Дело в том, что при операции выборки в Core Data обычно выполняется поисковый вызов. Затем этот вызов должен выбрать для вас определенные данные, обычно с помощью предиката. Чтобы сделать пользовательский интерфейс более отзывчивым, лучше всего выполнять такие операции выборки в фоновом контексте.
Вы можете создать в приложении столько контекстов, сколько захотите, однако помните об одном железном правиле. Нельзя передавать управляемые объекты между контекстами в разных потоках, так как объекты не являются потокобезопасными. Таким образом, если вы выбираете объекты в фоновом контексте, то не можете использовать их в главном потоке. Вот как следует передавать управляемые объекты между потоками: объект выбирается в фоновом потоке, а потом переносится в главный контекст (контекст, работающий в основном потоке). Это делается с помощью метода objectWithID: главного контекста. Этот метод принимает объект типа NSManagedObjectID. Поэтому в фоновом потоке мы на самом деле не выбираем управляемые объекты как таковые, а лишь берем их сохраняемые ID, после чего передаем эти ID главному контексту, который сам получает для вас управляемый объект. Итак, вы выполняете в фоновом режиме и поиск, и выборку объектов, затем передаете ID найденных объектов главному контексту. Получением самих объектов занимается уже главный контекст. Если действовать таким образом, главный контекст будет располагать сохраняемыми ID объектов, а получение этих объектов из постоянного хранилища в такой ситуации протекает гораздо быстрее, чем при выполнении полномасштабного поиска в главном контексте.
В этом разделе предполагается, что вы уже создали модель управляемых объектов Person. Подобная модель показана на рис. 16.20.
Рис. 16.20. Простая модель Core Data, используемая в этом разделе
При работе с этой моделью я заполню стек 1000 объектов Person, как показано в следующем коде, а уже потом попробую выбирать информацию из стека:
— (void) populateDatabase{
for (NSUInteger counter = 0; counter < 1000; counter++){
Person *person =
[NSEntityDescription
insertNewObjectForEntityForName: NSStringFromClass([Person class])
inManagedObjectContext: self.managedObjectContext];
person.firstName = [NSString stringWithFormat:@"First name %lu",
(unsigned long)counter];
person.lastName = [NSString stringWithFormat:@"Last name %lu",
(unsigned long)counter];
person.age = @(counter);
}
NSError *error = nil;
if ([self.managedObjectContext save:&error]){
NSLog(@"Managed to populate the database.");
} else {
NSLog(@"Failed to populate the database. Error = %@", error);
}
}
Обратите внимание: я использую класс NSStringFromClass для преобразования имени класса Person в строку и для последующего инстанцирования объектов такого типа. Некоторые программисты предпочитают типизировать Person как строковый литерал. Но если жестко запрограммировать вашу строку таким образом, может возникнуть проблема. Допустим, позже вы решите изменить имя Person в стеке Core Data, а жестко закодированная строка никуда не денется. Она может привести к аварийному завершению вашего приложения во время исполнения, так как объекта модели с именем Person больше не существует. Но если вы примените вышеупомянутую функцию для преобразования имени класса в обычную строку, то при изменении имени класса или отсутствии такого класса получите ошибку времени компиляции. Такие ошибки выявляются еще до ввода приложения в работу, и у вас будет время их исправить.
Прежде чем продолжать обсуждение, оговорюсь: предполагается, что вы уже заполнили базу данных с помощью последнего написанного нами метода. Далее в общих чертах изложено, как мы собираемся выполнять выборку в фоновом контексте.
1. Создаем фоновый контекст с помощью метода-инициализатора initWithConcurrencyType:, относящегося к классу NSManagedObjectContext, затем передаем этому методу значение NSPrivateQueueConcurrencyType. В результате получаем контекст, имеющий собственную закрытую очередь диспетчеризации. Поэтому, если вызвать в контексте блок performBlock:, этот блок будет выполнен в закрытой фоновой очереди.
2. Затем мы собираемся задать в фоновом контексте значение свойства persistentStoreCoordinator, которое будет равно экземпляру координатора нашего постоянного хранилища данных. Таким образом мы свяжем фоновый контекст с координатором постоянного хранилища. В результате, если выполнить выборку в фоновом контексте, эта операция позволит получить данные прямо с диска или из любого другого места, где их может хранить координатор.
3. Выполняем в фоновом контексте вызов performBlock:, а затем даем запрос на выборку. В рамках этого запроса требуется найти в стеке Core Data всех людей, чей возраст относится к диапазону от 100 до 200. Подчеркиваю: реалистичность эксперимента нас в данном случае не волнует. Я хочу лишь продемонстрировать, как действует выборка данных в фоновом режиме. Создавая запрос выборки данных, мы устанавливаем его свойство resultType в значение NSManagedObjectIDResultType. Так мы гарантируем, что результаты, возвращаемые после выполнения этого запроса на выборку, состоят не из управляемых объектов как таковых, а только из ID этих объектов. Как объяснялось ранее, мы не собираемся выбирать сами управляемые объекты, поскольку при выборке этих объектов в фоновом контексте не сможем использовать их в основном потоке. Итак, в фоновом контексте мы только выбираем их ID, а преобразуем эти ID в реальные управляемые объекты уже в главном контексте. После этого такие объекты можно использовать в основном потоке.
Вот как создается запрос на выборку:
— (NSFetchRequest *) newFetchRequest{
NSFetchRequest *request = [[NSFetchRequest alloc]
initWithEntityName:
NSStringFromClass([Person class])];
request.fetchBatchSize = 20;
request.predicate =
[NSPredicate predicateWithFormat:@"(age >= 100) AND (age <= 200)"];
request.resultType = NSManagedObjectIDResultType;
return request;
}
А вот как мы будем создавать фоновый контекст и выполнять в нем запрос на выборку данных:
— (BOOL) application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions{
__weak NSManagedObjectContext *mainContext = self.managedObjectContext;
__weak AppDelegate *weakSelf = self;
__block NSMutableArray *mutablePersons = nil;
/* Создаем фоновый контекст */
NSManagedObjectContext *backgroundContext =
[[NSManagedObjectContext alloc]
initWithConcurrencyType: NSPrivateQueueConcurrencyType];
backgroundContext.persistentStoreCoordinator =
self.persistentStoreCoordinator;
/* Выполняем блок в фоновом контексте */
[backgroundContext performBlock: ^{
NSError *error = nil;
NSArray *personIds = [backgroundContext
executeFetchRequest: [weakSelf newFetchRequest]
error:&error];
if (personIds!= nil && error == nil){
mutablePersons = [[NSMutableArray alloc]
initWithCapacity: personIds.count];
/* Теперь переходим в главный контекст и получаем эти объекты
в главном контексте, исходя из их ID */
dispatch_async(dispatch_get_main_queue(), ^{
for (NSManagedObjectID *personId in personIds){
Person *person = (Person *)[mainContext
objectWithID: personId];
[mutablePersons addObject: person];
}
[weakSelf processPersons: mutablePersons];
});
} else {
NSLog(@"Failed to execute the fetch request.");
}
}];
self.window = [[UIWindow alloc]
initWithFrame: [[UIScreen mainScreen] bounds]];
self.window.backgroundColor = [UIColor whiteColor];
[self.window makeKeyAndVisible];
return YES;
}
Этот код собирает все управляемые объекты в виде массива, а затем вызывает в делегате нашего приложения метод processPersons:, обрабатывающий результаты массива. Напишем этот метод следующим образом:
— (void) processPersons:(NSArray *)paramPersons{
for (Person *person in paramPersons){
NSLog(@"First name = %@, last name = %@, age = %ld",
person.firstName,
person.lastName,
(long)person.age.integerValue);
}
}
См. также
Разделы 7.4, 16.4 и 16.6.
16.10. Использование специальных типов данных в модели Core Data
Постановка задачи
Вы считаете, что набор типов данных, представленных в Core Data, не удовлетворяет стоящим перед вами требованиям. Вам хотелось бы использовать в объектах моделей и дополнительные типы данных, например UIColor. Но такие объекты не содержатся в Core Data в готовом виде.
Решение
Используйте преобразуемые типы данных.
Обсуждение
Core Data позволяет создавать для объектов моделей свойства, а потом присваивать этим свойствам типы данных. Выбор при этом довольно ограничен: Core Data допускает использование лишь таких типов данных, которые могут быть преобразованы в экземпляр NSData и обратно. Но существует целый ряд популярных классов, которые вы по умолчанию не можете использовать в таких свойствах. Что же делать? Применяйте преобразуемые свойства. Сначала поясню, что это такое.
Допустим, вы хотите создать в Core Data объект модели и назвать этот объект Laptop. У данного объекта будет два свойства: model типа String и color, которое должно относиться к типу UIColor. В Core Data не предоставляется такой тип данных, поэтому для его получения нам придется создать подкласс от NSValueTransformer. Назовем этот класс ColorTransformer. Вот что станем делать при его реализации.
1. Переопределим его метод класса allowsReverseTransformation и вернем от него значение YES. Так мы сообщим Core Data о возможности преобразования цветов в данные и обратно.
2. Переопределим метод transformedValueClass этого класса и возвратим от него имя класса NSData. Возвращаемое значение этого метода сообщает Core Data, в какой класс вы будете преобразовывать специальное значение. В данном случае происходит преобразование UIColor в NSData. Поэтому вы должны вернуть от этого метода имя класса NSData.
3. Переопределим метод экземпляра transformedValue:, относящийся к преобразователю. В нашем методе будем брать входящее значение (которое в данном случае будет экземпляром UIColor), преобразовывать его в NSData и возвращать эти данные.
4. Переопределим метод экземпляра reverseTransformedValue:, относящийся к преобразователю, и сделаем это с совершенно противоположной целью. Берем входящее значение (здесь — данные) и преобразуем его в цвет.
Имея всю эту информацию, продолжим реализацию преобразователя. Чтобы сохранять цвет как данные, просто разделим его на целочисленные компоненты, которые будут сохраняться в массиве:
#import
#import «ColorTransformer.h»
@implementation ColorTransformer
+ (BOOL) allowsReverseTransformation{
return YES;
}
+ (Class) transformedValueClass{
return [NSData class];
}
— (id) transformedValue:(id)value{
/* Преобразуем цвет в данные */
UIColor *color = (UIColor *)value;
CGFloat red, green, blue, alpha;
[color getRed:&red green:&green blue:&blue alpha:&alpha];
CGFloat components[4]О том, что такое аутлет и чем такая связь отличается от action, подробно рассказано в статье по адресу http://habrahabr.ru/post/30553 /. — Примеч. пер .
= {red, green, blue, alpha};
NSData *dataFromColors = [[NSData alloc] initWithBytes: components
length: sizeof(components)];
return dataFromColors;
}
— (id) reverseTransformedValue:(id)value{
/* Выполняем обратное преобразование из данных в цвет */
NSData *data = (NSData *)value;
CGFloat components[4]О том, что такое аутлет и чем такая связь отличается от action, подробно рассказано в статье по адресу http://habrahabr.ru/post/30553 /. — Примеч. пер .
= {0.0f, 0.0f, 0.0f, 0.0f};
[data getBytes: components length: sizeof(components)];
UIColor *color = [UIColor colorWithRed: components[0]
green: components[1]Подробнее об этих элементах см.: http://habrahabr.ru/post/79280 /. — Примеч. пер.
blue: components[2]Odd (англ.) — «нечетный», delete (англ.) — «удалить». — Примеч. пер.
alpha: components[3]Первая строка: «Потенциально неполная реализация метода». Вторая строка: «Неполная реализация метода». — Примеч. пер .
];
return color;
}
@end
Теперь возвращаемся к модели данных. Создадим управляемый объект Laptop и его атрибуты/свойства. Убедитесь, что атрибут color является преобразуемым. Выделив этот атрибут, нажмите на клавиатуре Alt+Command+3 и откройте Model Inspector (Инспектор модели) для этого атрибута. В поле name преобразуемого класса введите имя специального преобразователя. В данном случае это будет ColorTransformer (рис. 16.21).
Рис. 16.21. Создание модели с преобразуемым атрибутом
Теперь воспользуемся приемами, изученными в разделе 16.2, и сгенерируем файл класса для управляемого объекта Laptop. После этого перейдем к заголовочному файлу этого управляемого объекта. Как видите, атрибут color рассматриваемого класса относится к типу id:
#import
#import
@interface Laptop: NSManagedObject
@property (nonatomic, retain) NSString * model;
@property (nonatomic, retain) id color;
@end
Уже неплохо. Но чтобы сделать код еще лучше, в частности помочь компилятору выявлять возможные проблемы (они могут возникать, если присваивать этому свойству значения неподходящих типов), вручную изменим этот тип данных на UIColor:
#import
#import
/* Обязательно импортируем эту информацию в таком виде, в каком
UIColor находится в UIKit */
#import
@interface Laptop: NSManagedObject
@property (nonatomic, retain) NSString * model;
@property (nonatomic, retain) UIColor *color;
@end
Итак, осталось объединить весь изученный материал и применить его на практике. В делегате нашего приложения создадим экземпляр Laptop и зададим для него красный цвет. Затем вставим этот объект в стек Core Data и попытаемся считать его обратно. Так мы проверим, удалось ли успешно сохранить цветовое значение и вновь достать его из базы данных:
#import «AppDelegate.h»
#import «Laptop.h»
@implementation AppDelegate
@synthesize managedObjectContext = _managedObjectContext;
@synthesize managedObjectModel = _managedObjectModel;
@synthesize persistentStoreCoordinator = _persistentStoreCoordinator;
— (BOOL) application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions{
self.window = [[UIWindow alloc]
initWithFrame: [[UIScreen mainScreen] bounds]];
self.window.backgroundColor = [UIColor whiteColor];
[self.window makeKeyAndVisible];
/* Сначала сохраняем объект laptop с заданным цветом */
Laptop *laptop =
[NSEntityDescription
insertNewObjectForEntityForName: NSStringFromClass([Laptop class])
inManagedObjectContext: self.managedObjectContext];
laptop.model = @"model name";
laptop.color = [UIColor redColor];
NSError *error = nil;
if ([self.managedObjectContext save:&error] == NO){
NSLog(@"Failed to save the laptop. Error = %@", error);
}
/* Теперь находим этот же laptop */
NSFetchRequest *fetch =
[[NSFetchRequest alloc]
initWithEntityName: NSStringFromClass([Laptop class])];
fetch.fetchLimit = 1;
fetch.predicate = [NSPredicate predicateWithFormat:@"color == %@",
[UIColor redColor]];
error = nil;
NSArray *laptops = [self.managedObjectContext
executeFetchRequest: fetch
error:&error];
/* Проверка на 1, поскольку лимит выборки равен 1 */
if (laptops.count == 1 && error == nil){
Laptop *fetchedLaptop = laptops[0];
if ([fetchedLaptop.color isEqual: [UIColor redColor]]){
NSLog(@"Right colored laptop was fetched");
} else {
NSLog(@"Could not find the laptop with the given color.");
}
}
else {
NSLog(@"Could not fetch the laptop with the given color. \
Error = %@", error);
}
return YES;
}
См. также
Раздел 16.1.