Как стать автором
Обновить

Core Data для iOS. Глава №3. Теоретическая часть

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



Содержание:



Вступление

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

Интерфейс

Наше приложение назовём League Manager и состоять оно будет из 4 экранов:
  • Список команд
  • Добавить/редактировать команду
  • Список игроков
  • Добавить /редактировать игрока


Экран со списком команд отображает сохраненные локально футбольные команды с цветами их формы. На экране присутствует кнопка + для добавления новой команды и кнопка Edit для удаления. Вот как выглядит экран:
image

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

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

Как и с командами, при нажатии на + появляется экран добавления нового игрока в команду, а при нажатии на Edit можно редактировать информацию об игроке.
image

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

Использование SQLite в качестве хранилища

Запускаем ХКод и создаем новый проект.
image

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

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

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

Удалите (нажатие клавиши Delete) уже имеющуюся сущность Event, которая нам не пригодится.

Начнем с создания сущности Team:
image

Создадим два атрибута у сущности Team:
  • name (String)
  • uniformColor (String)

Получим следующую картину:
image

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

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

image

Связь «один-ко-многим»

Для создания новой связи между сущностями Team и Player необходимо сперва выбрать сущность Team и в разделе Relationships нажать + и назвать новую связь players, для Destination выбрать сущность Player. Справа установите флаг «To-Many Relationship» при выбранной связи players. В качестве правила удаления выберите Cascade (при удалении команды будут автоматически удалены все игроки команды).
image

Теперь необходимо создать связь со стороны игрока (сущности Player). Добавляем сущности Player связь под названием team, в поле Destination выбираем Team, а в поле Invers выбираем players (после этого не забываем установить поле Inverse для связи players у сущности Team)
image

Построение пользовательского интерфейса

После завершения создания модели данных необходимо заняться написанием кода для отображения этих самых данных. ХКод автоматически генерирует весь необходимый код для отображения списка команд; следующим заданием будет немного подкорректировать его для работы с сущностями 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:
image
Назовем этот класс TeamViewController и не забываем установить галочку на опции With XIB for user interface.
image

Открываем 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, он у нас изначально пустой должен быть. Устанавливаем туда две надписи, два поля ввода текста и две кнопк, связываем действия кнопок с соответствующими методами-обработчиками. Итоговый вид примерно такой:
image

Перед тем, как запускать приложение на выполнение, вернемся в 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];
}

Запустите приложение на выполнение:
image

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

Можно заметить, что мы можем создавать команды с пустыми именами или цветами форм. Если бы это было реальное приложение, то нам необходимо было принять некоторые обеспечительные меры от плохих пользовательских данных. Подобный «промах» с проверкой данных был нами намеренно оставлен, ибо в Главе №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. Теперь соберем и запустим приложение. В списке команд мы видим ранее созданные нами команды, при нажатии на аксессуар-кнопку открывается список игроков выбранной команды (пока списки игроков пусты, ибо мы не реализовали добавление нового игрока).
image

Добавление, редактирование и удаление игроков

Приложение уже почти готово; единственное, что стоит реализовать, так это добавление/редактирование/удаление игроков.
Создадим новый контроллер (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 с соответствующими кнопками.
image

Для того, чтобы отобразить этот экран необходимо вернуться к реализации метода 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. После добавления игроков вы должны будете увидеть их в списке.
image
image

Перевыполним команду в 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), однако такие ситуации есть. Например, локальное кэширование данных, полученных с удаленного сервера. Представьте ситуацию, когда ваше приложение обрабатывает данные с удаленного сервера. Если ваше приложение производит достаточно много запросов, хорошей практикой будет ускорить ответы на эти запросы выбрав наиболее «отзывчивый» тип хранилища. Удаленный сервер может передавать данные в сжатом виде, на мобильном устройстве будет происходить распаковка и загрузка данных в память для ускорения последующих запросов. В таком случае вы хотите, чтобы данные каждый раз обновлялись (либо обновлялись с некоторой периодичностью) и потеря данных при завершении работы приложения не является проблемой.
image

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

Разработка собственного типа хранилища

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

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

Настраиваемые хранилища данных должны наследоваться от NSAtomicStore класса (подкласса NSPersistentStore), который предоставляет возможности (методы) необходимые для работы с данными. Для того, чтобы лучше понять, каким образом это работает, представьте два внутренних слоя внутри Core Data Framework, как показано на картинке ниже:
image

Пользователь взаимодействует со слоями 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'а.
Теги:
Хабы:
+20
Комментарии 4
Комментарии Комментарии 4

Публикации

Истории

Работа

Ближайшие события

Московский туристический хакатон
Дата 23 марта – 7 апреля
Место
Москва Онлайн
Геймтон «DatsEdenSpace» от DatsTeam
Дата 5 – 6 апреля
Время 17:00 – 20:00
Место
Онлайн
PG Bootcamp 2024
Дата 16 апреля
Время 09:30 – 21:00
Место
Минск Онлайн
EvaConf 2024
Дата 16 апреля
Время 11:00 – 16:00
Место
Москва Онлайн