Хабралюди, добрый день!
Сегодня хочу начать написание ряда лекций с практическими заданиями по книге Михаеля Привата и Роберта Варнера «Pro Core Data for iOS», которую можете купить по этой ссылке. Каждая глава будет содержать теоретическую и практическую часть.

Содержание:
В этой главе мы создадим приложение с двумя таблицами и «один-ко-многим» отношением между ними. Представьте, что вы вызвались добровольцем следить за командами из младшей футбольной лиги и их составом. В нашей модели данных будет две таблицы: Team (хранит информацию о наименовании команды и цвете их формы) и Player (хранит информацию об игроке команды — почтовый ящик, имя и фамилию). Понятно, что у одной команды множество игроков, а игрок принадлежит одной команде.
Создадим мы это приложение сперва с использованием SQLite в качестве хранилища, а потом попробуем использовать другие варианты (хранение в памяти и произвольные хранилища (atomic store)).
Наше приложение назовём League Manager и состоять оно будет из 4 экранов:
Экран со списком команд отображает сохраненные локально футбольные команды с цветами их формы. На экране присутствует кнопка

Экран добавления/редактирования футбольной команды состоит из двух полей для ввода наименования команды и цвета их формы. Данный экран отображается после нажатия на кнопку

Экран со списком игроков отображает всех игроков определенной команды. Переход на данный экран осуществляется после нажатия на синюю стрелочку на экране «Список команд» напротив каждой команды:

Как и с командами, при нажатии на

Теперь, когда мы знаем что нам предстоит делать, можем приступать.
Запускаем ХКод и создаем новый проект.

Называем его LeagueManager и в качестве идентификатора компании пишем book.coredata.

После создания проекта:

На данном этапе создания приложения, самым важным компонентом и частью является создание модели данных. Откроем

Удалите (нажатие клавиши
Начнем с создания сущности

Создадим два атрибута у сущности
Получим следующую картину:

Для того, чтобы установить один тип атрибута сразу нескольким атрибутам, то вам достаточно выделить их и справа выбрать нужный тип:

Далее создаем сущность

Для создания новой связи между сущностями

Теперь необходимо создать связь со стороны игрока (сущности

После завершения создания модели данных необходимо заняться написанием кода для отображения этих самых данных. ХКод автоматически генерирует весь необходимый код для отображения списка команд; следующим заданием будет немного подкорректировать его для работы с сущностями
Нам необходим метод для добавления команды в хранилище, давайте добавим его в файл
Код в
Откройте MasterViewController.m и подкорректируйте наименование экрана следующим образом:
Теперь реализуем метод добавления новой команды:
Теперь найдем строку примерно следующего содержания:
и заменим на:
Далее необходимо внести еще одно маленькое изменение в существующий код, заменить эту строку:
на:
После создания нового объекта мы устанавливаем его свойствам значения, которые были переданы:
После того, как свойства объекта были изменены нам необходимо его сохранить, поэтому и вызывается метод
Подкорректируем еще один метод следующим образом:
Ячейки таблицы всё еще сконфигурированы для отображения сущностей
Меняем эту строку:
на:
И меняем тип создаваемых ячеек с таких:
на:
В сгенерированном коде присутствует метод
Пока наше приложение мало что делает. Оно не может создать команду, не может создать игрока. Сейчас подходящий момент запустить приложение на выполнение, чтобы убедиться, что мы на правильном пути и всё работает. Если по каким-то причинам у вас появляются ошибки при сборке приложения, то попробуйте пройти все шаги заново, перепроверьте модель данных.
При создании приложения из шаблона
Удалите строку
После чего мы можем спокойно удалять все три файла
Создаем новый класс, родителем нашего класса будет являться

Назовем этот класс

Открываем
Открываем
В случае, если пользователь приложения захочет создать новую команду, то параметр aTeam будет равен nil, и контроллер
Теперь осталось реализовать обработчики событий при нажатии на кнопки Save и Cancel:
Метод
После того, как написан код для взаимодействия с элементами пользовательского интерфейса, мы можем приступить собственно к созданию этого самого пользовательского интерфейса. Открываем

Перед тем, как запускать приложение на выполнение, вернемся в
Идем в
Теперь осталось «связать» нажатие на + с действием. Переходим в
на этот:
На данном этапе приложение уже может создавать команды, но перед тем, как запустить приложение на выполнение предлагаю добавить сразу код для редактирования команд:
Запустите приложение на выполнение:

Теперь мы можем добавлять команды, редактировать их. Добавьте несколько команд, в списке они будут отсортированы в лексикографическом порядке.


Можно заметить, что мы можем создавать команды с пустыми именами или цветами форм. Если бы это было реальное приложение, то нам необходимо было принять некоторые обеспечительные меры от плохих пользовательских данных. Подобный «промах» с проверкой данных был нами намеренно оставлен, ибо в Главе №5 мы изучим способы валидации данных.
Закройте приложение, запустите снова… наслаждайтесь вашими трудами… но, мы закончили только работу с командами, а с игроками еще предстоит поработать. Этим мы и займемся в следующем разделе.
Для реализации пользовательского интерфейса управления игроками нам понадобится два экрана и соответствующих контроллера: один для отображения списка игроков в команде, а второй для добавления нового игрока или редактирования уже существующего. Эти контроллеры в больше части являются зеркальными копиями контроллеров для работы с командами, хотя они и не содержат
Создадим сперва контроллер и экран для отображения списка игроков команды. Создаем новый контроллер
На экране будет кнопка + для добавления нового игрока. Объявим соответствующий метод-обработчик нажатия.
PlayerListViewController.h
Откройте файл
Сгенерированный автоматически метод
А метод
Подкорректируем метод viewWillAppear: следующим образом:
Список игроков в таблице будет отсортирован в алфавитном порядке в единственной секции (разделе). Для того, чтобы получить список всех игроков команды необходимо вызвать метод
Выше есть вызов метода
Для того, чтобы отображать список игроков команды вернемся в MasterViewController.m и добавим метод, который будет обрабатывать нажатие на дополнительный аксессуар (синий элемент ячейки):
Добавьте импорт

Приложение уже почти готово; единственное, что стоит реализовать, так это добавление/редактирование/удаление игроков.
Создадим новый контроллер (
PlayerViewController.h
Теперь откройте
В метод
Следующим нашим шагом будет реализация методов-обработчиков нажатий на кнопки:
Метода
Делегатом, который будет обрабатывать действия совершенные в
Вернемся теперь к
Открываем теперь
Последним шагом является создание пользовательского экрана для добавления/редактирования игрока. Выберите

Для того, чтобы отобразить этот экран необходимо вернуться к реализации метода
Нам также необходимо обрабатывать нажатия на ячейки таблицы списка игроков. Находим в
На этоv реализация приложения управления командами и игроками завершена. Запустите приложение. Добавьте игроков, удалите игроков, удалите команды с игроками.
Во второй главе мы уже разбирались, как работать с
Держите ваше приложение League Manager запущенным, чтобы вы могли переключаться между
Начнем с того, что посмотрим, какие же таблицы были созданы:
Создайте три команды: Crew (Blue), Fire (Red), Revolution (Green).
В SQLite базе данных они будет выглядеть примерно следующим образом (зависит от того сколько команд вы создали и удалили):
Как показывает быстрая проверка, игроков в League Manager приложении еще нет:
Откройте в приложении список игроков команды Crew и добавьте трёх новых: Jordan Gordon, Pat Sprat, Bailey Staley. После добавления игроков вы должны будете увидеть их в списке.


Перевыполним команду в
Теперь добавим нового игрока в команду Fire и назовем его Terry Gary. Выведем список всех игроков команд и наименование команды в которой он играет:
Откройте приложение, удалите игрока Pat Sprat команды Crew и выполните запрос заново:
Наконец удалим команду Fire и, стоит заметить, что удалилась не только сама команда, но и её единственный игрок Terry Gary:
Не бойтесь заглядывать под капот Core Data.
В большинстве случаев ваше приложение будет использовать в качестве хранилища данных SQLite базу данных, поэтому понимание того, как Core Data работает с ней может помочь избежать трудностей и повысить производительность приложения.
В предыдущей секции мы построили приложение использующее Core Data и в качестве хранилища данных — SQLite. В этом разделе речь пойдет об альтернативном типе хранилища: хранилища в памяти. Давайте взглянем на то, каким образом можно изменить тип используемого хранилища данных.
Изменить тип используемого хранилища просто, достаточно лишь при создании
Вот как будет выглядеть модифицированный метод
Тип хранилища данных был изменен на хранилище в памяти.
Первое, что мы заметим при очередном запуске приложение будет то, что все ранее добавленные данные исчезли. Это произошло потому, что мы изменили тип хранилища данных и не произвели миграцию данных из старого в новое. В Главе №8 мы рассмотрим, каким образом можно осуществлять миграцию данных между двумя хранилищами.
Жизненный цикл хранилища в памяти начинается при инициализации стека Core Data и завершается при остановке выполнения приложения.
Примечание
Размышляя о различных типах хранилищ данных, которые предоставляются по умолчанию, сложно с первого раза придумать ситуацию в которой стоило бы использовать хранилище в памяти (in-memory), однако такие ситуации есть. Например, локальное кэширование данных, полученных с удаленного сервера. Представьте ситуацию, когда ваше приложение обрабатывает данные с удаленного сервера. Если ваше приложение производит достаточно много запросов, хорошей практикой будет ускорить ответы на эти запросы выбрав наиболее «отзывчивый» тип хранилища. Удаленный сервер может передавать данные в сжатом виде, на мобильном устройстве будет происходить распаковка и загрузка данных в память для ускорения последующих запросов. В таком случае вы хотите, чтобы данные каждый раз обновлялись (либо обновлялись с некоторой периодичностью) и потеря данных при завершении работы приложения не является проблемой.

При разработке ваших последующих приложений с использованием Core Data, задумайтесь об использовании in-memory хранилища каждый раз, когда приложению нет необходимости хранить данные между запусками. В традиционных приложениях, в которых необходимо хранить пользовательские данные, подобный тип хранилища не будет пользоваться популярностью.
Основным принципом Core Data является абстрагирование от типа реального хранилища. Подобное абстрагирование позволяет изменить тип внутреннего хранилища, предоставляемого по-умолчанию (NSSQLiteStoreType, NSInMemoryStoreType, NSBinaryStoreType), без необходимости менять более одной строки. В некоторых случаях, хранилище данных по-умолчанию, может не удовлетворять вашим потребностям. На этот случай у Core Data для вас есть подарок — возможность самому создать произвольный тип хранилища данных. В этой секции мы создадим новый тип хранилища и будем использовать его в нашем приложении League Manager.
Перед тем как мы погрузимся в реализацию, стоит помнить, что Core Data позволяет создать только атомарные типы хранилищ. Атомарный тип хранилища — это такой тип, при котором операция сохранения осуществляет сохранение всех данных при каждом своём вызове целиком. К сожалению, подобное ограничение не позволяет создать и использовать что-то более эффективное нежели SQLite базу данных. В этой секции мы разработаем файловый тип хранилища, данные в нём будут храниться с использованием разделителя запятой (CSV), а для разделения значений будем использовать вертикальную черту (|).
Настраиваемые хранилища данных должны наследоваться от

Пользователь взаимодействует со слоями
Новое настраиваемое хранилище данных отвечает за перенос данных между хранилищем на устройстве и NSAtomicStoreCacheNodes, так же, как и переносом данных между NSManagedObjects и NSAtomicStoreCacheNodes.
Первый шаг на пути к созданию настраиваемого хранилища данных — создание класса для нового типа хранилища. Настраиваемое хранилище, которое будет разрабатываться в этой секции, полностью будет находиться в классе
CustomStore.h
CustomStore.m
Все хранилища в Core Data имеют некий набор метаданных, который позволяет
Для создания уникальных идентификаторов, добавим следующий метод класса:
В примере из этой главы, два файлы необходимы для работы настраиваемого хранилища данных. Первый файл, который имеет расширение txt, содержит непосредственно сами данные; второй файл, коорый имеет расширение plist содержит метаданные. Для решения вопроса о загрузке и сохранении метаданных мы добавим еще один метод и закончим реализацию метода
Следующий метод сохраняет файл с метаданными:
Загрузка же метаданных немного сложнее происходит, ибо в случае первой загрузки (обращению) к хранилищу, необходимо создать файл с метаданными вместе с пустым файлом данных (txt в нашем случае). Core Data ожидает получить из метаданных тип хранилища и UUID, которые позволяют определиться с тем, как работать с данным типом хранилища, а значит необходимо установить значения для ключей
Имея в наличии методы загрузки/сохранения метаданных, мы можем завершить метод инициализации настраиваемого хранилища данных следующим образом:
Для того, чтобы наше настраиваемое хранилище данных работала нужным образом, необходимо реализовать еще три дополнительных метода. Первый из методов создаёт новый ссылающийся объект для переданного
Второй метод, который нам пригодится, создаёт экземпляр
В реализации
Сегодня хочу начать написание ряда лекций с практическими заданиями по книге Михаеля Привата и Роберта Варнера «Pro Core Data for iOS», которую можете купить по этой ссылке. Каждая глава будет содержать теоретическую и практическую часть.

Содержание:
- Глава №1. Приступаем (Практическая часть)
- Глава №2. Усваиваем Core Data (Практическая часть)
- Глава №3. Хранение данных: SQLite и другие варианты
- Глава №4. Создание модели данных
- Глава №5. Работаем с объектами данных
- Глава №6. Обработка результатирующих множеств
- Глава №7. Настройка производительности и используемой памяти
- Глава №8. Управление версиями и миграции
- Глава №9. Управление таблицами с использованием NSFetchedResultsController
- Глава №10. Использование Core Data в продвинутых приложениях
Вступление
В этой главе мы создадим приложение с двумя таблицами и «один-ко-многим» отношением между ними. Представьте, что вы вызвались добровольцем следить за командами из младшей футбольной лиги и их составом. В нашей модели данных будет две таблицы: Team (хранит информацию о наименовании команды и цвете их формы) и Player (хранит информацию об игроке команды — почтовый ящик, имя и фамилию). Понятно, что у одной команды множество игроков, а игрок принадлежит одной команде.
Создадим мы это приложение сперва с использованием SQLite в качестве хранилища, а потом попробуем использовать другие варианты (хранение в памяти и произвольные хранилища (atomic store)).
Интерфейс
Наше приложение назовём League Manager и состоять оно будет из 4 экранов:
- Список команд
- Добавить/редактировать команду
- Список игроков
- Добавить /редактировать игрока
Экран со списком команд отображает сохраненные локально футбольные команды с цветами их формы. На экране присутствует кнопка
+
для добавления новой команды и кнопка Edit
для удаления. Вот как выглядит экран:
Экран добавления/редактирования футбольной команды состоит из двух полей для ввода наименования команды и цвета их формы. Данный экран отображается после нажатия на кнопку
+
на экране «Список команд» для добавления новой команды, либо при нажатии на одну из команд из списка для её редактирования.
Экран со списком игроков отображает всех игроков определенной команды. Переход на данный экран осуществляется после нажатия на синюю стрелочку на экране «Список команд» напротив каждой команды:

Как и с командами, при нажатии на
+
появляется экран добавления нового игрока в команду, а при нажатии на Edit
можно редактировать информацию об игроке.
Теперь, когда мы знаем что нам предстоит делать, можем приступать.
Использование SQLite в качестве хранилища
Запускаем ХКод и создаем новый проект.

Называем его LeagueManager и в качестве идентификатора компании пишем book.coredata.

После создания проекта:

На данном этапе создания приложения, самым важным компонентом и частью является создание модели данных. Откроем
*.xcdatamodeld
в ХКоде:
Удалите (нажатие клавиши
Delete
) уже имеющуюся сущность Event
, которая нам не пригодится.Начнем с создания сущности
Team
:
Создадим два атрибута у сущности
Team
:- name (String)
- uniformColor (String)
Получим следующую картину:

Для того, чтобы установить один тип атрибута сразу нескольким атрибутам, то вам достаточно выделить их и справа выбрать нужный тип:

Далее создаем сущность
Player
с тремя атрибутами:- firstName (String)
- lastName (String)
- email (String)

Связь «один-ко-многим»
Для создания новой связи между сущностями
Team
и Player
необходимо сперва выбрать сущность Team
и в разделе Relationships
нажать +
и назвать новую связь players, для Destination
выбрать сущность Player
. Справа установите флаг «To-Many Relationship» при выбранной связи players. В качестве правила удаления выберите Cascade
(при удалении команды будут автоматически удалены все игроки команды).
Теперь необходимо создать связь со стороны игрока (сущности
Player
). Добавляем сущности Player
связь под названием team
, в поле Destination
выбираем Team
, а в поле Invers
выбираем players
(после этого не забываем установить поле Inverse
для связи players
у сущности Team
)
Построение пользовательского интерфейса
После завершения создания модели данных необходимо заняться написанием кода для отображения этих самых данных. ХКод автоматически генерирует весь необходимый код для отображения списка команд; следующим заданием будет немного подкорректировать его для работы с сущностями
Team
, а не Event
.Нам необходим метод для добавления команды в хранилище, давайте добавим его в файл
MasterViewController.m
, предварительно не забыв описать в MasterViewController.h
.Код в
MasterViewController.h
выглядит теперь следующим образом:#import <UIKit/UIKit.h>
#import <CoreData/CoreData.h>
@interface MasterViewController : UITableViewController <NSFetchedResultsControllerDelegate>
@property (nonatomic, strong) NSFetchedResultsController *fetchedResultsController;
@property (nonatomic, strong) NSManagedObjectContext *managedObjectContext;
- (void)insertTeamWithName:(NSString *)name uniformColor:(NSString *)uniformColor;
- (void)saveContext;
@end
Откройте MasterViewController.m и подкорректируйте наименование экрана следующим образом:
self.title = NSLocalizedString(@"League Manager", @"League Manager");
Теперь реализуем метод добавления новой команды:
- (void)insertTeamWithName:(NSString *)name uniformColor:(NSString *)uniformColor
{
NSManagedObjectContext *context = [self.fetchedResultsController managedObjectContext];
NSEntityDescription *entity = [[self.fetchedResultsController fetchRequest] entity];
NSManagedObject *newManagedObject = [NSEntityDescription insertNewObjectForEntityForName:[entity name] inManagedObjectContext:context];
[newManagedObject setValue:name forKey:@"name"];
[newManagedObject setValue:uniformColor forKey:@"uniformColor"];
[self saveContext];
}
Теперь найдем строку примерно следующего содержания:
NSEntityDescription *entity = [NSEntityDescription entityForName:@"Event" inManagedObjectContext:self.managedObjectContext];
и заменим на:
NSEntityDescription *entity = [NSEntityDescription entityForName:@"Team" inManagedObjectContext:self.managedObjectContext];
Далее необходимо внести еще одно маленькое изменение в существующий код, заменить эту строку:
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] iniWithKey:@"timestamp" ascending:NO];
на:
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] iniWithKey:@"name" ascending:NO];
После создания нового объекта мы устанавливаем его свойствам значения, которые были переданы:
[newManagedObject setValue:name forKey:@"name"];
[newManagedObject setValue:uniformColor forKey:@"uniformColor"];
После того, как свойства объекта были изменены нам необходимо его сохранить, поэтому и вызывается метод
saveContext
.- (void)saveContext
{
NSManagedObjectContext *context = [self.fetchedResultsController managedObjectContext];
NSError *error = nil;
if(![context save:&error]){
NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
abort();
}
}
Подкорректируем еще один метод следующим образом:
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath
{
if (editingStyle == UITableViewCellEditingStyleDelete) {
[self saveContext];
}
}
Настройка таблицы
Ячейки таблицы всё еще сконфигурированы для отображения сущностей
Event
, вместо требуемых Team
сущностей. Нам необходимо отобразить в одной ячейке таблицы две составляющие: имя команды и цвет формы в которой играет эта команда. Для того, чтобы достичь этого необходимо сперва изменить стиль отображаемой ячейки таблицы, как и идентификатор CellIdentifier
, используемый в методе cellForRowAtIndexPath:
.Меняем эту строку:
static NSString* CellIdentifier = @"Cell";
на:
static NSString* CellIdentifier = @"TeamCell";
И меняем тип создаваемых ячеек с таких:
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
на:
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleValue1 reuseIdentifier:CellIdentifier];
В сгенерированном коде присутствует метод
configureCell:atIndexPath:
, в котором собственно происходит настройка ячейки для отображение. Но в текущем виде, метод работает с сущностью Event
, а не Team
, поэтому необходимо внести кое-какие коррективы.- (void)configureCell:(UITableViewCell *)cell atIndexPath:(NSIndexPath *)indexPath {
NSManagedObject *managedObject = [self.fetchedResultsController objectAtIndexPath:indexPath];
cell.textLabel.text = [[managedObject valueForKey:@"name"] description];
cell.detailTextLabel.text = [[managedObject valueForKey:@"uniformColor"] description];
cell.accessoryType = UITableViewCellAccessoryDetailDisclosureButton;
}
Создание команды
Пока наше приложение мало что делает. Оно не может создать команду, не может создать игрока. Сейчас подходящий момент запустить приложение на выполнение, чтобы убедиться, что мы на правильном пути и всё работает. Если по каким-то причинам у вас появляются ошибки при сборке приложения, то попробуйте пройти все шаги заново, перепроверьте модель данных.
При создании приложения из шаблона
Master-Details View Application
создается еще один контроллер с названием DetailViewController
. Вы можете использовать этот класс, но так как нам необходимо отображать информацию о команде и игроках, то лучше избавиться от этого контроллера и создавать новые контроллеры с соответствующими именами.Удалите строку
#import DetailViewController.h
из MasterViewController.m
. Найдите метод tableView:didSelectRowAtIndexPath:
и очистите его тело. Выглядит следующим образом:- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
}
После чего мы можем спокойно удалять все три файла
DetailViewController
(.h, .m, .xib). Если мы запустим приложение и нажмем на кнопку +, то приложение аварийно прекращает свою работу. Этот + по прежнему привязан к методу insertNewObject:
, который мы удалили. Нам необходимо привязать к этой кнопке возможность создания новой команды, а точнее — показать модальное окно с полями для ввода информации (наименование команды и цвет формы) о новой команде. Это же окно будет использоваться для редактирования уже имеющихся команд при нажатии на ячейку с командой на экране списка команд. Создаем новый класс, родителем нашего класса будет являться
UIViewController
:
Назовем этот класс
TeamViewController
и не забываем установить галочку на опции With XIB for user interface
.
Открываем
TeamViewController.h
. В League Manager, класс MasterViewController
управляет NSManagedObjectContext
, значит в TeamViewController
будет необходима ссылка на эту среду управления объектами и соответствующий метод инициализации. Так как данный контроллер будет отвечать и за редактирование информации о команде, то при инициализации стоит передавать объект команды (для этого нам так же нужно будет создать свойство и добавить в метод инициализации). Пользовательский интерфейс экрана добавления/редактирования команды будет содержать два текстовых поля — для наименования команды и цвета их формы, для них необходимо создать соответствующие свойства в TeamViewController
. На данном экране так же будут находиться две кнопки — кнопка сохранения (Save) новой команды и кнопка отмены (Cancel). В TeamViewController
должны быть методы-обработчики нажатий на эти кнопки.TeamViewController.h
#import <UIKit/UIKit.h>
@class MasterViewController;
@interface TeamViewController : UIViewController {
IBOutlet UITextField *name;
IBOutlet UITextField *uniformColor;
NSManagedObject *team;
MasterVIewController *masterController;
}
@property (nonatomic, retain) UITextField *name;
@property (nonatomic, retain) UITextField *uniformColor;
@property (nonatomic, retain) NSManagedObject *team;
@property (nonatomic, retain) MasterViewController *masterController;
- (IBAction)save:(id)sender;
- (IBAction)cancel:(id)sender;
- (id)initWithMasterController:(MasterViewController *)aMasterController team:(NSManagedObject *)aTeam;
@end
Открываем
TeamViewController.m
, импортируем MasterViewController.h
, удаляем метод initWithNibName:
, добавляем synthesize для name
, team
и masterController
. Добавляем вот такой метод инициализации:- (id)initWithMasterController:(MasterController *)aMasterController team:(NSManagedObject *)aTeam {
if((self = [super init])){
self.masterController = aMasterController;
self.team = aTeam;
}
return self;
}
В случае, если пользователь приложения захочет создать новую команду, то параметр aTeam будет равен nil, и контроллер
TeamViewController.m
будет отвечать за создание нового NSManagedObject
объекта. В случае же, если пользователь выберет одну из существующих команд для редактирования, то за контроллером остаётся ответственность заполнить текстовые поля на экране соответствующими данными из командного объекта (имя команды и цвет формы). Последний функционал мы добавим в метод viewDidLoad
:- (void)viewDidLoad{
[super viewDidLoad];
if(team != nil){
name.text = [team valueForKey:@"name"];
uniformColor.text = [team valueForKey:@"uniformColor"];
}
}
Теперь осталось реализовать обработчики событий при нажатии на кнопки Save и Cancel:
- (IBAction)save:(id)sender
{
if(masterController != nil){
if(team != nil){
[team setValue:name.text forKey:@"name"];
[team setValue:uniformColor.text forKey:@"uniformColor"];
[masterController saveContext];
} else {
[masterController insertNewTeamWithName:name.text uniformColor:uniformColor.text];
}
}
[self dismissModalViewControllerAnimated:YES];
}
Метод
cancel:
просто убирает окно редактирования/добавления команды.TeamViewController.m
#import "TeamViewController.h"
#import "MasterViewController.h"
@implementation TeamViewController
@synthesize name;
@synthesize uniformColor;
@synthesize team;
@synthesize masterController;
- (id)initWithMasterController:(MasterController *)aMasterController team:(NSManagedObejct *)aTeam {
if((self = [super init])){
self.masterController = aMasterController;
self.team = aTeam;
}
return self;
}
- (void)didReceiveMemoryWarning{
[super didReceiveMemoryWarning];
}
#pragma mark - View lifecycle
- (void)viewDidLoad{
[super viewDidLoad];
if(team != nil){
name.text = [team valueForKey:@"name"];
uniformColor.text = [team valueForKey:@"uniformColor"];
}
}
- (void)viewDidUnload{
[super viewDidUnload];
}
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation {
return (interfaceOrientation == UIInterfaceOrientationPortrait);
}
#pragma mark - Button handlers
- (IBAction)save:(id)sender{
if(masterController != nil){
if(team != nil){
[team setValue:name.text forKey:@"name"];
[team setValue:uniformColor.text forKey:@"uniformColor"];
[masterController saveContext];
} else {
[masterController insertTeamWithName:name.text uniformColor:uniformColor.text];
}
}
[self dismissModalViewControllerAnimated:YES];
}
- (IBAction)cancel:(id)sender{
[self dismissModalViewControllerAnimated:YES];
}
@end
После того, как написан код для взаимодействия с элементами пользовательского интерфейса, мы можем приступить собственно к созданию этого самого пользовательского интерфейса. Открываем
TeamViewController.xib
, он у нас изначально пустой должен быть. Устанавливаем туда две надписи, два поля ввода текста и две кнопк, связываем действия кнопок с соответствующими методами-обработчиками. Итоговый вид примерно такой:
Перед тем, как запускать приложение на выполнение, вернемся в
MasterViewController
и добавим код для отображения экрана с информацией о команде. Отображать экран редактирования команды мы должны в двух случаях: 1) пользователь нажал на + 2) пользователь нажал на команду из списка. Начнем с нажатия на кнопку +. Объявите новый метод в MasterViewController.h
:- (void)showTeamView;
Идем в
MasterViewController.m
, импортируем TeamViewController.h
и реализуем указанный выше метод следующим образом:- (void)showTeamView{
TeamViewController *teamViewController = [[TeamViewController alloc] initWithMasterController:self team:nil];
[self presentModalViewController:teamViewController animated:YES];
}
Теперь осталось «связать» нажатие на + с действием. Переходим в
viewDidLoad
метод и заменяем вот этот код:UIBarButtonItem *addButton = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemAdd target:self action:@selector(insertNewObject)];
на этот:
UIBarButtonItem *addButton = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemAdd target:self action:@selector(showTeamView)];
На данном этапе приложение уже может создавать команды, но перед тем, как запустить приложение на выполнение предлагаю добавить сразу код для редактирования команд:
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath)indexPath{
NSManagedObject *team = [[self fetchedResultsController] objectAtIndexPath:indexPath];
TeamViewController *teamViewController = [[TeamViewController alloc] initWithMasterController:self team:team];
[self presentModalViewController:teamViewController animated:YES];
}
Запустите приложение на выполнение:

Теперь мы можем добавлять команды, редактировать их. Добавьте несколько команд, в списке они будут отсортированы в лексикографическом порядке.


Можно заметить, что мы можем создавать команды с пустыми именами или цветами форм. Если бы это было реальное приложение, то нам необходимо было принять некоторые обеспечительные меры от плохих пользовательских данных. Подобный «промах» с проверкой данных был нами намеренно оставлен, ибо в Главе №5 мы изучим способы валидации данных.
Закройте приложение, запустите снова… наслаждайтесь вашими трудами… но, мы закончили только работу с командами, а с игроками еще предстоит поработать. Этим мы и займемся в следующем разделе.
Пользовательский интерфейс для управления игроками
Для реализации пользовательского интерфейса управления игроками нам понадобится два экрана и соответствующих контроллера: один для отображения списка игроков в команде, а второй для добавления нового игрока или редактирования уже существующего. Эти контроллеры в больше части являются зеркальными копиями контроллеров для работы с командами, хотя они и не содержат
NSFetchedResultsController
и остальной код для работы с Core Data, вместо этого они делегируют взаимодействие с Core Data MasterViewController
.Создадим сперва контроллер и экран для отображения списка игроков команды. Создаем новый контроллер
PlayerListViewController
, устанавливаем его родительский класс UITableViewController
и снимаем галочку с опции «With XIB for user interface». Открываем файл PlayerListViewController.h
. Этот класс отвечает за отображение списка игроков команды, а значит в этом классе необходима ссылка на объект команды. Так же, с учетом того, что данный класс делегирует взаимодействие с Core Data контроллеру MasterViewController
, то необходима еще и ссылка на сам контроллер. На экране будет кнопка + для добавления нового игрока. Объявим соответствующий метод-обработчик нажатия.
PlayerListViewController.h
#import <UIKit/UIKit.h>
@class MasterViewController;
@interface PlayerListViewController : UITableVIewController {
NSManagedObject *team;
MasterViewController *masterViewController;
}
@property (nonatomic, retain) NSManagedObject *team;
@property (nonatomic, retain) MasterViewController *masterController;
- (id)initWithMasterController:(MasterViewController *)aMasterController team:(NSManagedObject *)aTeam;
- (void)showPlayerView;
- (NSArray *)sortPlayers;
@end
Откройте файл
PlayerListViewController.m
и импортируйте MasterVIewController.h
, синтезируйте свойства team
и masterController
. Измените сгенерированный метод initWithStyle:
на initWithMasterController:
, который принимает два свойства и сохраняет следующим образом:- (id)initWithMasterController:(MasterVIewController *)aMasterVIewController team:(NSManagedObject *)aTeam {
if((self = [super init])){
self.masterController = aMasterController;
self.team = aTeam;
}
return self;
}
Сгенерированный автоматически метод
viewDidLoad
изменим следующим образом:- (void)viewDidLoad{
[super viewDidLoad];
self.title = @"Player";
UIBarButtonItem *addButton = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemAdd target:self action:@selector(showPlayerView)];
self.navigationItem.rightBarButtonItem = addButton;
}
А метод
showPlayerView
пока оставим пустым:- (void)showPlayerView{
}
Подкорректируем метод viewWillAppear: следующим образом:
- (void)viewWillAppear:(BOOL)animated{
[super viewWillAppear:animated];
[self.tableView reloadData];
}
Список игроков в таблице будет отсортирован в алфавитном порядке в единственной секции (разделе). Для того, чтобы получить список всех игроков команды необходимо вызвать метод
valueForKey:@"players"
объекта команды, который вернет нам NSSet*
игроков. Ниже представлен код для настройки отображения таблицы:- (NSUInteger)numberOfSectionInTableView:(UITableView *)tableView {
return 1;
}
- (NSInteger)tableView:(UITableVIew *)tableView numberOfRowsInSection:(NSInteger)section
{
return [(NSSet *)[team valueForKey:@"players"] count];
}
- (UITableViewCell *)tableView:(UITableVIew *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *CellIdentifier = @"PlayerCell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if(cell == nil){
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleValue1 reuseIdentifier:CellIdentifier];
}
NSManagedObject *player = [[self sortPlayers] objectAtIndex:indexPath.row];
cell.textLabel.text = [NSString stringWithFormat:@"%@ %@", [[player valueForKey:@"firstName"] description], [[player valueForKey:@"lastName"] description]];
cell.detailTextLabel.text = [[player valueForKey:@"email"] description];
return cell;
}
Выше есть вызов метода
sortPlayers
, который возвращает отсортированный массив игроков:- (NSArray *)sortPlayers{
NSSortDescriptor *sortLastNameDescriptor = [[NSSortDescriptor alloc] initWithKey:@"lastName" ascending:YES];
NSArray *sortDescriptors = [NSArray arrayWithObjects:sortLastNameDescriptor, nil];
return [[(NSSet *)[team valueForKey:@"players"] allObjects] sortedArrayUsingDescriptors:sortDescriptors];
}
Для того, чтобы отображать список игроков команды вернемся в MasterViewController.m и добавим метод, который будет обрабатывать нажатие на дополнительный аксессуар (синий элемент ячейки):
- (void)tableView:(UITableVIew *)tableView accessoryButtonTappedForRowWithIndexPath:(NSIndexPath *)indexPath{
NSManagedObject *team = [self.fetchedResultsController objectAtIndexPath:indexPath];
PlayerListViewController *playerListViewController = [[PlayerListViewController alloc] initWithMasterController:self team:team];
[self.navigationController pushViewController:playerListViewController animated:YES];
}
Добавьте импорт
PlayerListViewController.h
в MasterViewController.m
. Теперь соберем и запустим приложение. В списке команд мы видим ранее созданные нами команды, при нажатии на аксессуар-кнопку открывается список игроков выбранной команды (пока списки игроков пусты, ибо мы не реализовали добавление нового игрока).
Добавление, редактирование и удаление игроков
Приложение уже почти готово; единственное, что стоит реализовать, так это добавление/редактирование/удаление игроков.
Создадим новый контроллер (
UIViewController
) c соответствующим XIBом и назовем его PlayerViewController
. Он будет похож на TeamViewController
, но содержать три поля: lastName
, firstName
и email
. Контроллер так же содержит ссылку на MasterViewController
для того, чтобы иметь возможность использовать написанные ранее методы для работы с Core Data. Так же будут присутствовать еще два свойства: команды в которой играет игрок и сам игрок. Если объект игрока равен nil
, то PlayerViewController
знает, что необходимо создать нового игрока, в противном случае — редактирование данных игрока. На экране у нас будет три кнопки: сохранить, отмена и удалить. При запросе на удаление игрока мы будем запрашивать подтверждение у пользователя в виде отображения UIActionSheeta
, поэтому необходимо, чтобы PlayerViewController
реализовывал методы протокола UIActionSheetDelegate
. PlayerViewController.h
#import <UIKit/UIKit.h>
@class MasterViewController;
@interface PlayerViewController : UIViewController <UIActionSheetDelegate> {
IBOutlet UITextField *firstName;
IBOutlet UITextField *lastName;
IBOutlet UITextField *email;
NSManagedObject *team;
NSManagedObject *player;
MasterViewController *masterViewController;
}
@property (nonatomic, retain) UITextField *firstName;
@property (nonatomic, retain) UITextField *lastName;
@property (nonatomic, retain) UITextField *email;
@property (nonatomic, retain) NSManagedObject *team;
@property (nonatomic, retain) NSManagedObject *player;
@property (nonatomic, retain) MasterViewController *masterController;
- (IBAction)save:(id)sender;
- (IBAction)cancel:(id)sender;
- (IBAction)confirmDelete:(id)sender;
- (id)initWithMasterController:(MasterViewController *)aMasterController team:(NSManagedObject *)aTeam player:(NSManagedObject *)aPlayer;
@end
Теперь откройте
PlayerViewController.m
, импортируйте MasterViewController.h
и добавьте @synthesize
для всех свойств из интерфейса. Добавьте метод инициализации в PlayerVIewController.m
, который будет получать экземпляр класса MasterViewController
, команду и возможно объект игрока.- (id)initWithMasterController:(MasterViewController *)aMasterController team:(NSManagedObject *)aTeam player:(NSManagedObject *)aPlayer{
if((self = [super init])){
self.masterController = aMasterController;
self.team = team;
self.player = player;
}
return self;
}
В метод
viewDidLoad
добавим код для заполнения текстовых полей данными игрока, если он не равен nil
.- (void)viewDidLoad {
[super viewDidLoad];
if(player != nil){
firstName.text = [player valueForKey:@"firstName"];
lastName.text = [player valueForKey:@"lastName"];
email.text = [player valueForKey:@"email"];
}
}
Следующим нашим шагом будет реализация методов-обработчиков нажатий на кнопки:
- (IBAction)save:(id)sender{
if(masterController != nil){
if(player != nil){
[player setValue:firstName.text forKey:@"firstName"];
[player setValue:lastName.text forKey:@"lastName"];
[player setValue:email.text forKey:@"email"];
} else {
[masterController insertPlayerWithTeam:team firstName:firstName.text lastName:lastName.text email:email.text];
}
}
[self dismissModelViewControllerAnimated:YES];
}
- (IBAction)cancel:(id)sender{
[self dismissModalViewControllerAnimated:YES];
}
Метода
insertPlayerWithTeam:firstName:lastName:email:
в контроллере MasterViewController
пока нет, но мы его напишем буквально через пару минут. Сперва реализуем confirmDelete:
метод, который вызывается при нажатии на кнопку «Delete»(Удалить). Этот метод не будет сразу удалять игрока, а он запросит у пользователя подтверждения на выполнение данного действия (делается для того, чтобы избежать случайных нажатий и удалений игроков). Вот как будет выглядеть метод confirmDelete:
:- (IBAction)confirmDelete:(id)sender{
if(player != nil){
UIActionSheet *confirm = [[UIActionSheet alloc] initWithTitle:nil delegate:self cancelButtonTitle:@"Cancel" destructiveButtonTitle:@"Delete Player" otherButtonTitles:nil];
confirm.actionSheetStyle = UIActionSheetStyleBlackTranslucent;
[confirm showInView:self.view];
}
}
Делегатом, который будет обрабатывать действия совершенные в
UIActionSheetе
будет текущий класс. При нажатии на кнопки UIActionSheetа
будет вызываться метод clickedButtonAtIndex:
, а значит необходимо его реализовать. В методе будет проверка на то какая кнопка была нажата и, если кнопка Delete, то будет вызван метод (который мы так же потом реализуем) удаления игрока:- (void)actionSheet:(UIActionSheet *)actionSheet clickedButtonAtIndex:(NSInteger)buttonIndex{
if(buttonIndex == 0 && masterController != nil){
[masterController deletePlayer:player];
[self dismissModalViewControllerAnimated:YES];
}
}
Вернемся теперь к
MasterViewController.h
и объявим два метода, которые мы еще не реализовывали, но уже использовали:- (void)insertPlayerWithTeam:(NSManagedObject *)team firstName:(NSString *)firstName lastName:(NSString *)lastName email:(NSString *)email;
- (void)deletePlayer:(NSManagedObject *)player;
Открываем теперь
MasterViewController.m
и реализуем методы:- (void)insertPlayerWithTeam:(NSManagedObject *)team firstName:(NSString *)firstName lastName:(NSString *)lastName email:(NSString *)email{
NSManagedObjectContext *context = [self.fetchedResultsController managedObjectContext];
NSManagedObject *player = [NSEntityDescription insertNewObjectForEntityForName:@"Player" inManagedObjectContext:context];
[player setValue:firstName forKey:@"firstName"];
[player setValue:lastName forKey:@"lastName"];
[player setValue:email forKey:@"email"];
[player setValue:team forKey:@"team"];
[self saveContext];
}
- (void)deletePlayer:(NSManagedObject *)player{
NSManagedObjectContext *context = [self.fetchedResultsController managedObjectContext];
[context deleteObject:player];
[self saveContext];
}
Последним шагом является создание пользовательского экрана для добавления/редактирования игрока. Выберите
PlayerViewController.xib
, приведите его к такому виду, как на картинке ниже и соедините все Action с соответствующими кнопками.
Для того, чтобы отобразить этот экран необходимо вернуться к реализации метода
showPlayerView:
, который мы ранее использовали. Импортируйте в PlayerListViewController.m
файл PlayerViewController.h
. - (void)showPlayerVIew{
PlayerVIewController *playerViewController = [[PlayerVIewController alloc] initWithMasterController:masterController team:team player:nil];
[self presentModalViewController:playerVIewController animated:YES];
}
Нам также необходимо обрабатывать нажатия на ячейки таблицы списка игроков. Находим в
PlayerListViewController.m
автоматически сгенерированный метод didSelectRowAtIndexPath:
и приводим его к следующему виду:- (void)tableView:(UITableVIew *)tableVIew didSelectRowAtIndexPath:(NSIndexPath *)indexPath{
NSManagedObject *player = [[self sortPlayers] objectAtIndex:indexPath.row];
PlayerViewController *playerViewController = [[PlayerVIewController alloc] initWithMasterController:masterController team:team player:player];
[self presentModalVIewController:playerViewController animated:YES];
}
На этоv реализация приложения управления командами и игроками завершена. Запустите приложение. Добавьте игроков, удалите игроков, удалите команды с игроками.
Проверка данных хранилища
Во второй главе мы уже разбирались, как работать с
sqlite3
для изучения структуры БД, которую генерирует Core Data. В завершение секции о работе с хранилищами типа SQLite отыщем нашу БД League_Manager.sqlite3
и запустим sqlite3
передав в качестве входного параметра имя нашей базы.sqlite3 ./5.0/Applications/CE79C20B-4CBF-47C3–9E7C- 9EC24FA22488/Documents/League_Manager.sqlite
Держите ваше приложение League Manager запущенным, чтобы вы могли переключаться между
sqlite3
и им и, следить что происходит.Начнем с того, что посмотрим, какие же таблицы были созданы:
sqlite> .tables
ZPLAYER ZTEAM Z_METADATA Z_PRIMARYKEY
ZPLAYER
хранит данные сущности Player
; ZTEAM
— хранит данные сущности Team
.Создайте три команды: Crew (Blue), Fire (Red), Revolution (Green).
В SQLite базе данных они будет выглядеть примерно следующим образом (зависит от того сколько команд вы создали и удалили):
sqlite> select * from ZTEAM;
1|2|3|Crew|Blue
2|2|1|Fire|Red
3|2|1|Revolution|Green
Как показывает быстрая проверка, игроков в League Manager приложении еще нет:
sqlite> select * from ZPLAYER;
Откройте в приложении список игроков команды Crew и добавьте трёх новых: Jordan Gordon, Pat Sprat, Bailey Staley. После добавления игроков вы должны будете увидеть их в списке.


Перевыполним команду в
sqlite3
по отображению всех игроков в приложении:sqlite> select * from ZPLAYER;
1|1|1|1|Jordan|Gordan|jgordon@example.com
2|1|1|1|Pat|Sprat|psprat@example.com
3|1|1|1|Bailey|Staley|bstaley@example.com
Теперь добавим нового игрока в команду Fire и назовем его Terry Gary. Выведем список всех игроков команд и наименование команды в которой он играет:
sqlite> select ZTEAM.ZNAME, ZPLAYER.ZFIRSTNAME, ZPLAYER.ZLASTNAME from ZTEAM, ZPLAYER where ZTEAM.Z_PK = ZPLAYER.ZTEAM;
Crew|Jordan|Gordon
Crew|Pat|Sprat
Crew|Bailey|Staley Fire|Terry|Gary
Откройте приложение, удалите игрока Pat Sprat команды Crew и выполните запрос заново:
sqlite> select ZTEAM.ZNAME, ZPLAYER.ZFIRSTNAME, ZPLAYER.ZLASTNAME from ZTEAM, ZPLAYER where ZTEAM.Z_PK = ZPLAYER.ZTEAM;
Crew|Jordan|Gordon
Crew|Bailey|Staley
Fire|Terry|Gary
Наконец удалим команду Fire и, стоит заметить, что удалилась не только сама команда, но и её единственный игрок Terry Gary:
sqlite> select ZTEAM.ZNAME, ZPLAYER.ZFIRSTNAME, ZPLAYER.ZLASTNAME from ZTEAM, ZPLAYER where ZTEAM.Z_PK = ZPLAYER.ZTEAM;
Crew|Jordan|Gordon
Crew|Bailey|Staley
Не бойтесь заглядывать под капот Core Data.
В большинстве случаев ваше приложение будет использовать в качестве хранилища данных SQLite базу данных, поэтому понимание того, как Core Data работает с ней может помочь избежать трудностей и повысить производительность приложения.
Использование хранилища данных в памяти (In-memory persistent store)
В предыдущей секции мы построили приложение использующее Core Data и в качестве хранилища данных — SQLite. В этом разделе речь пойдет об альтернативном типе хранилища: хранилища в памяти. Давайте взглянем на то, каким образом можно изменить тип используемого хранилища данных.
Изменить тип используемого хранилища просто, достаточно лишь при создании
NSPersistentStoreCoordinator
указать другой тип, нежели NSSQLiteStoreType
.Вот как будет выглядеть модифицированный метод
persistentStoreCoordinator:
в League_ManagerAppDelegate.m
файле:- (NSPersistentStoreCoordinator *)persistentStoreCoordinator {
if(_persistentStoreCoordinator != nil)
return _persistentStoreCoordinator;
// NSURL *storeURL = [[self applicationDocumentsDirectory] URLByAppendingPathComponent:@"League_Manager.sqlite"];
NSError *error = nil;
_persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[self managedObjectModel]];
if(![_persistentStoreCoordinator addPersistentStoreWithType:NSInMemoryStoreType configuration:nil URL:nil options:nil error:&error]){
NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
abort();
}
return _persistentStoreCoordinator;
}
Тип хранилища данных был изменен на хранилище в памяти.
Первое, что мы заметим при очередном запуске приложение будет то, что все ранее добавленные данные исчезли. Это произошло потому, что мы изменили тип хранилища данных и не произвели миграцию данных из старого в новое. В Главе №8 мы рассмотрим, каким образом можно осуществлять миграцию данных между двумя хранилищами.
Жизненный цикл хранилища в памяти начинается при инициализации стека Core Data и завершается при остановке выполнения приложения.
Примечание
Начиная с iOS 4 и введением многозадачности, переключение на другое приложение не обязательно приводит к завершению работы вашего приложения. Вместо завершения, наше приложение продолжает работу в фоновом режиме, а значит и данные в памяти прододжают оставаться.
Размышляя о различных типах хранилищ данных, которые предоставляются по умолчанию, сложно с первого раза придумать ситуацию в которой стоило бы использовать хранилище в памяти (in-memory), однако такие ситуации есть. Например, локальное кэширование данных, полученных с удаленного сервера. Представьте ситуацию, когда ваше приложение обрабатывает данные с удаленного сервера. Если ваше приложение производит достаточно много запросов, хорошей практикой будет ускорить ответы на эти запросы выбрав наиболее «отзывчивый» тип хранилища. Удаленный сервер может передавать данные в сжатом виде, на мобильном устройстве будет происходить распаковка и загрузка данных в память для ускорения последующих запросов. В таком случае вы хотите, чтобы данные каждый раз обновлялись (либо обновлялись с некоторой периодичностью) и потеря данных при завершении работы приложения не является проблемой.

При разработке ваших последующих приложений с использованием Core Data, задумайтесь об использовании in-memory хранилища каждый раз, когда приложению нет необходимости хранить данные между запусками. В традиционных приложениях, в которых необходимо хранить пользовательские данные, подобный тип хранилища не будет пользоваться популярностью.
Разработка собственного типа хранилища
Основным принципом Core Data является абстрагирование от типа реального хранилища. Подобное абстрагирование позволяет изменить тип внутреннего хранилища, предоставляемого по-умолчанию (NSSQLiteStoreType, NSInMemoryStoreType, NSBinaryStoreType), без необходимости менять более одной строки. В некоторых случаях, хранилище данных по-умолчанию, может не удовлетворять вашим потребностям. На этот случай у Core Data для вас есть подарок — возможность самому создать произвольный тип хранилища данных. В этой секции мы создадим новый тип хранилища и будем использовать его в нашем приложении League Manager.
Перед тем как мы погрузимся в реализацию, стоит помнить, что Core Data позволяет создать только атомарные типы хранилищ. Атомарный тип хранилища — это такой тип, при котором операция сохранения осуществляет сохранение всех данных при каждом своём вызове целиком. К сожалению, подобное ограничение не позволяет создать и использовать что-то более эффективное нежели SQLite базу данных. В этой секции мы разработаем файловый тип хранилища, данные в нём будут храниться с использованием разделителя запятой (CSV), а для разделения значений будем использовать вертикальную черту (|).
Настраиваемые хранилища данных должны наследоваться от
NSAtomicStore
класса (подкласса NSPersistentStore
), который предоставляет возможности (методы) необходимые для работы с данными. Для того, чтобы лучше понять, каким образом это работает, представьте два внутренних слоя внутри Core Data Framework, как показано на картинке ниже:
Пользователь взаимодействует со слоями
NSManagedObject
и NSManagedObjectContext
. Второй слой непосредственно производит сохранение данных и содержит хранилища данных и координатора хранилищ данных. В случае настраиваемых типов хранилищ, слой с хранилищем данных так же содержит NSAtomicStoreCacheNode
, который хранит объекты содержащие сами данные. Отношение NSAtomicStoreCacheNode
к NSAtomicStore
такое же, как NSManagedObject
к NSManagedObjectContext
. Инициализация настраиваемого хранилища
Новое настраиваемое хранилище данных отвечает за перенос данных между хранилищем на устройстве и NSAtomicStoreCacheNodes, так же, как и переносом данных между NSManagedObjects и NSAtomicStoreCacheNodes.
Первый шаг на пути к созданию настраиваемого хранилища данных — создание класса для нового типа хранилища. Настраиваемое хранилище, которое будет разрабатываться в этой секции, полностью будет находиться в классе
CustomStore
. Добавьте в League Manager новый класс, который будет наследоваться от NSAtomicStore.CustomStore.h
#import <Foundation/Foundation.h>
@interface CustomStore : NSAtomicStore {
}
@end
CustomStore.m
#import "CustomStore.h"
@implementation CustomStore
#pragma mark - NSPersistentStore
- (NSString *)type {
return [[self metadata] objectForKey:NSStoreTypeKey];
}
- (NSString *)identifier {
return [[self metadata] objectForKey:NSStoreUUIDKey];
}
- (id)initWithPersistentStoreCoordinator:(NSPersistentStoreCoordinator *)coordinator configurationName:(NSString *)configurationName URL:(NSURL *)url options:(NSDictionary *)options {
self = [super initWithPersistentStoreCoordinator:coordinator configurationName:configurationName URL:url options:options];
return self;
}
+ (NSDictionary *)metadataForPersistentStoreWithURL:(NSURL *)url error:(NSError **)error {
return nil;
}
#pragma mark - NSAtomicStore
- (BOOL)load:(NSError **)error {
return YES;
}
- (id)newReferenceObjectForManagedObject:(NSManagedObject *)managedObject {
return nil;
}
- (NSAtomicStoreCacheNode *)newCacheNodeForManagedObject:(NSManagedObject *)managedObject {
return nil;
}
- (BOOL)save:(NSError **)error {
return YES;
}
- (void)updateCacheNode:(NSAtomicStoreCacheNode *)node fromManagedObject:(NSManagedObject *)managedObject {
}
@end
Все хранилища в Core Data имеют некий набор метаданных, который позволяет
NSPersistentStoreCoordinator'у
управлять различными типами хранилищ. В NSPersistentStore
метаданных представлены в виде словаря NSDictionary
. Значения двух ключей представляют собой особый интерес: NSStoreTypeKey
и NSStoreUUIDKey
. Значение для ключа NSStoreTypeKey
должно представлять собой уникальную строку идентифицирующую тип хранилища, а NSStoreUUIDKey
— непосредственно само хранилище.Для создания уникальных идентификаторов, добавим следующий метод класса:
+ (NSString *)makeUUID {
CFUUIDRef uuidRef = CFUUIDCreate(NULL);
CFStringRef uuidStringRef = CFUUIDCreateString(NULL, uuidRef);
CFRelease(uuidRef);
NSString *uuid = [NSString stringWithString:(__bridge NSString *)uuidStringRef];
CFRelease(uuidStriingRef);
return uuid;
}
В примере из этой главы, два файлы необходимы для работы настраиваемого хранилища данных. Первый файл, который имеет расширение txt, содержит непосредственно сами данные; второй файл, коорый имеет расширение plist содержит метаданные. Для решения вопроса о загрузке и сохранении метаданных мы добавим еще один метод и закончим реализацию метода
metadataForPersistentStoreWithURL:error:
. Следующий метод сохраняет файл с метаданными:
+ (void)writeMetadata:(NSDictionary *)metadata toURL:(NSURL *)url {
NSString *path = [[url relativePath] stringByAppendingString:@".plist"];
[metadata writeToFile:path atomically:YES];
}
Загрузка же метаданных немного сложнее происходит, ибо в случае первой загрузки (обращению) к хранилищу, необходимо создать файл с метаданными вместе с пустым файлом данных (txt в нашем случае). Core Data ожидает получить из метаданных тип хранилища и UUID, которые позволяют определиться с тем, как работать с данным типом хранилища, а значит необходимо установить значения для ключей
NSStoreTypeKey
, NSStoreUUIDKey
. Найдите метод metadataForPersistentStoreWithURL:error:
и измените его тело таким образом, чтобы в нём происходила проверка на наличие файла метаданных и, если такого файла нет, то он создавался (с указанными ключами) вместе с пустым файлом хранилища (текстовым файлом).+ (NSDictionary *)metadataForPersistentStoreWithURL:(NSURL *)url error:(NSError **)error {
// determine the filename for metadata file
NSString *path = [[url relativePath] stringByAppendingString:@".plist"];
if(![[NSFileManager defaultManager] fileExistsAtPath:path]) {
// create a dictionary and store the store type key (CustomStore)
// and the UUID key
NSMutableDictionary *metadata = [NSMutableDictionary dictionary];
[metadata setValue:@"CustomStore" forKey:NSStoreTypeKey];
[metadata setValue:[CustomStore makeUUID] forKey:NSStoreUUIDKey];
// write the metadata to the .plist file
[CustomStore writeMetadata:metadata toURl:url];
// write an empty data file
[@"" writeToURL:url atomically:YES encoding:[NSString defaultCStringEncoding] error:nil];
NSLog(@"Created new store at %@", path);
}
return [NSDictionary dictionaryWithContentsOfFile:path];
}
Имея в наличии методы загрузки/сохранения метаданных, мы можем завершить метод инициализации настраиваемого хранилища данных следующим образом:
- (id)initWithPersistentStoreCoordinator:(NSPersistentStoreCoordinator *)coordinator configurationName:(NSString *)configurationName URL:(NSURL *)url options:(NSDictionary *)options {
self = [super initWithPersistentStoreCoordinator:coordinator configurationName:configurationName URL:url options:options];
NSDictionary *metadata = [CustomStore metadataForPersistentStoreWithURL:[self URL] error:nil];
[self setMetadata:metadata];
return self;
}
Связь между NSManagedObject и NSAtomicStoreCacheNode
Для того, чтобы наше настраиваемое хранилище данных работала нужным образом, необходимо реализовать еще три дополнительных метода. Первый из методов создаёт новый ссылающийся объект для переданного
NSManagedObject'а
. Ссылающиеся объекты представляют собой уникальные идентификаторы для каждой NSAtomicStoreCacheNode'ы
(аналогично первичному ключу, отношение такое же, как и NSObjectID
к NSManagedObject
). Так как настраиваемое хранилище отвечает за перевод между NSManagedObject'ами
в NSAtomicStoreCacheNode
, оно должно уметь создавать ссылающийся объект для только что созданных NSManagedObject'ов
. Для этого мы снова используем UUID:- (id)newReferenceObjectForManagedObject:(NSManagedObject *)managedObject {
NSString *uuid = [CustomStore makeUUID];
return uuid;
}
Второй метод, который нам пригодится, создаёт экземпляр
NSAtomicStoreCacheNode
класса для соответствующего NSManagedObject
объекта. Когда новый NSManagedObject
создаётся и у фрэймворка возникает необходимость произвести его сохранение, то происходит вызов метода newReferenceObjectForManagedObject:
. NSAtomicCache
следит за связями между NSObjectID
и ссылающимися объектами. Когда Core Data производит сохранение NSManagedObject'а в локальное хранилище, вызывается метод newCacheNodeForManagedObject:
, который, как видно по его имени, создаёт новый экземпляр NSAtomicStoreCacheNode
, который служит аналогом NSManagedObject.- (NSAtomicStoreCacheNode *)newCacheNodeForManagedObject:(NSManagedObject *)managedObject {
NSManagedObjectID *oid = [managedObject objectID];
id referenceID = [self referenceObjectForObjectID:oid];
NSAtomicStoreCacheNode* node = [self nodeForReferenceObject:referenceID andObjectID:oid];
[self updateCacheNode:node fromManagedObject:managedObject];
return node;
}
В реализации
newCacheNodeForManagedObject:
происходит поиск ссылающегося объекта, который был создан для соответствующего NSManagedObject объекта, и создание новой NSAtomicStoreCacheNode'ы с копированием всех полей из экземпляра NSManagedObject'а.