Introduction
Данная статья, вторая и заключительная статья из серии «Разбираем iPhone Core Data Recipes». Первую часть статьи, вы можете прочитать тут. Цель серии статей — помочь начинающему iOS разработчику, понять, как правильно работать с SQLite базой данных используя Core Data на примере iPhone Core Data Recipes. В заключительной статье мы рассмотрим функционал добавления, редактирования и удаления записей из базы данных.
Prerequisites
Для самостоятельного изучения исходных текстов данного приложения, вам необходим стандартный набор инструментов:
- Mac OS X
- Xcode
Данный набор позволит вам просмотреть, изменить и запустить приложение на симуляторе. В случае же, если вы захотите попробовать запустить его на настоящем iPhone, требуется участие в iOS Developer Program.
А также, что немало важно, нужно базовое понимание структуры языка Objective-C и приложения.
Ссылки на используемые материалы и инструменты предоставлены в разделе References.
How To Create a New Recipe
Итак, первое что нам необходимо — это создать новый рецепт. За создание нового рецепта, в данном проекте, отвечает view controller — RecipeAddViewController. Рассмотрим его содержимое.
RecipeAddViewController.h
@protocol RecipeAddDelegate; @class Recipe; @interface RecipeAddViewController : UIViewController <UITextFieldDelegate> { @private //Объект Recipe, уже привязанный managedObjectContext Recipe *recipe; //Поле для ввода названия рецепта UITextField *nameTextField; id <RecipeAddDelegate> delegate; } @property(nonatomic, retain) Recipe *recipe; @property(nonatomic, retain) IBOutlet UITextField *nameTextField; @property(nonatomic, assign) id <RecipeAddDelegate> delegate; //Сохраняем новый рецепт - (void)save; //Отменяем создание нового рецепта - (void)cancel; @end @protocol RecipeAddDelegate <NSObject> // recipe == nil on cancel - (void)recipeAddViewController:(RecipeAddViewController *)recipeAddViewController didAddRecipe:(Recipe *)recipe; @end
RecipeAddViewController.m
#import "RecipeAddViewController.h" #import "Recipe.h" @implementation RecipeAddViewController @synthesize recipe; @synthesize nameTextField; @synthesize delegate; - (void)viewDidLoad { // Конфигурируем navigation bar self.navigationItem.title = @"Add Recipe"; //Создаем кнопку Cancel и привязываем ее к действию cancel UIBarButtonItem *cancelButtonItem = [[UIBarButtonItem alloc] initWithTitle:@"Cancel" style:UIBarButtonItemStyleBordered target:self action:@selector(cancel)]; self.navigationItem.leftBarButtonItem = cancelButtonItem; [cancelButtonItem release]; //Создаем кнопку Save и привязываем ее к действию save UIBarButtonItem *saveButtonItem = [[UIBarButtonItem alloc] initWithTitle:@"Save" style:UIBarButtonItemStyleDone target:self action:@selector(save)]; self.navigationItem.rightBarButtonItem = saveButtonItem; [saveButtonItem release]; [nameTextField becomeFirstResponder]; } ... //Сохраняем новый рецепт - (void)save { recipe.name = nameTextField.text; NSError *error = nil; if (![recipe.managedObjectContext save:&error]) { NSLog(@"Unresolved error %@, %@", error, [error userInfo]); abort(); } [self.delegate recipeAddViewController:self didAddRecipe:recipe]; } //Отменяем создание нового рецепта - (void)cancel { [recipe.managedObjectContext deleteObject:recipe]; NSError *error = nil; if (![recipe.managedObjectContext save:&error]) { NSLog(@"Unresolved error %@, %@", error, [error userInfo]); abort(); } [self.delegate recipeAddViewController:self didAddRecipe:nil]; } //Т.к. проект не использует ARC, необходимо подчистить память - (void)dealloc { [recipe release]; [nameTextField release]; [super dealloc]; } @end
В итоге, форма добавления нового рецепта, выглядит следующим образом

Детальное рассмотрение действий save & cancel будет ниже.
Открываем форму создания нового рецепта

Кнопка для создания нового рецепта расположена в главном окне нашего приложения (см. скриншот выше). Рассмотрим её более делатьно.
Данная кнопка создается в методе viewDidLoad контроллера RecipeListTableViewController.
UIBarButtonItem *addButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemAdd target:self action:@selector(add:)]; self.navigationItem.rightBarButtonItem = addButtonItem; [addButtonItem release];
К данной кнопке привязано действие add, которое также находится в контроллере RecipeListTableViewController (см. комментарии в исходном коде).
- (void)add:(id)sender { //Создаем контроллер RecipeAddViewController который мы рассматривали выше RecipeAddViewController *addController = [[RecipeAddViewController alloc] initWithNibName:@"RecipeAddView" bundle:nil]; //Связываем его с RecipeListTableViewController addController.delegate = self; //В managedObjectContext создаем объект Recipe с помощью NSEntityDescription, другими словами - создаем новую запись в таблице Recipe Recipe *newRecipe = [NSEntityDescription insertNewObjectForEntityForName:@"Recipe" inManagedObjectContext:self.managedObjectContext]; //Передаем новый объект Recipe контроллеру RecipeAddViewController addController.recipe = newRecipe; //Модально оторбажаем контроллер RecipeAddViewController UINavigationController *navigationController = [[UINavigationController alloc] initWithRootViewController:addController]; [self presentModalViewController:navigationController animated:YES]; [navigationController release]; [addController release]; }
После выполнения данного действия, откроется форма добавления нового рецепта рассмотренная выше. На этой форме, для пользователя доступно два действия — Save & Cancel.
Сохраняем новый рецепт
- (void)save { //Свойству name объекта Recipe устанавливаем значение из поля recipe.name = nameTextField.text; NSError *error = nil; //Сохраняем новую запись if (![recipe.managedObjectContext save:&error]) { NSLog(@"Unresolved error %@, %@", error, [error userInfo]); abort(); } //Отображаем созданный рецепт с помощью RecipeDetailViewController (исходный текст для этого действия см. ниже в секции "Отображаем созданный рецепт") [self.delegate recipeAddViewController:self didAddRecipe:recipe]; }
Отменяем сохранение нового рецепта
- (void)cancel { //Удаляем ранее созданный объект Recipe, другими словами, удаляем ранее созданную запись в базе данных [recipe.managedObjectContext deleteObject:recipe]; NSError *error = nil; if (![recipe.managedObjectContext save:&error]) { NSLog(@"Unresolved error %@, %@", error, [error userInfo]); abort(); } //Отображаем список рецептов [self.delegate recipeAddViewController:self didAddRecipe:nil]; }
Отображаем созданный рецепт
- (void)recipeAddViewController:(RecipeAddViewController *)recipeAddViewController didAddRecipe:(Recipe *)recipe { if (recipe) { // Если объект Recipe не равняется nil - тогда отображаем его [self showRecipe:recipe animated:NO]; } [self dismissModalViewControllerAnimated:YES]; } - (void)showRecipe:(Recipe *)recipe animated:(BOOL)animated { // Создаем контроллер который отображает детальную информацию о рецепте, передаем в него рецепт и отображаем RecipeDetailViewController *detailViewController = [[RecipeDetailViewController alloc] initWithStyle:UITableViewStyleGrouped]; detailViewController.recipe = recipe; [self.navigationController pushViewController:detailViewController animated:animated]; [detailViewController release]; }
How To Create a New Ingredient

Как вы можете видеть на скриншоте выше, при просмотре детальной информации о рецепте, в режиме редактирования, доступна ссылка «Add Ingredient». Создается она в методе
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
контроллера RecipeDetailViewController.
static NSString *AddIngredientCellIdentifier = @"AddIngredientCell"; cell = [tableView dequeueReusableCellWithIdentifier:AddIngredientCellIdentifier]; if (cell == nil) { // Create a cell to display "Add Ingredient". cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:AddIngredientCellIdentifier] autorelease]; cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator; } cell.textLabel.text = @"Add Ingredient";
За добавление и редактирование ингредиентов отвечает контроллер IngredientDetailViewController.
IngredientDetailViewController.h
@class Recipe, Ingredient, EditingTableViewCell; @interface IngredientDetailViewController : UITableViewController { @private Recipe *recipe; Ingredient *ingredient; EditingTableViewCell *editingTableViewCell; } @property (nonatomic, retain) Recipe *recipe; @property (nonatomic, retain) Ingredient *ingredient; @property (nonatomic, assign) IBOutlet EditingTableViewCell *editingTableViewCell; @end
IngredientDetailViewController.m
... - (id)initWithStyle:(UITableViewStyle)style { if (self = [super initWithStyle:style]) { UINavigationItem *navigationItem = self.navigationItem; navigationItem.title = @"Ingredient"; UIBarButtonItem *cancelButton = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemCancel target:self action:@selector(cancel:)]; self.navigationItem.leftBarButtonItem = cancelButton; [cancelButton release]; UIBarButtonItem *saveButton = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemSave target:self action:@selector(save:)]; self.navigationItem.rightBarButtonItem = saveButton; [saveButton release]; } return self; } ... - (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation { return (interfaceOrientation != UIInterfaceOrientationPortraitUpsideDown); } ... - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { //Отображаем только две строки, название и количество return 2; } //Создаем поля для отображения и редактирования - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { static NSString *IngredientsCellIdentifier = @"IngredientsCell"; EditingTableViewCell *cell = (EditingTableViewCell *)[tableView dequeueReusableCellWithIdentifier:IngredientsCellIdentifier]; if (cell == nil) { [[NSBundle mainBundle] loadNibNamed:@"EditingTableViewCell" owner:self options:nil]; cell = editingTableViewCell; self.editingTableViewCell = nil; } if (indexPath.row == 0) { cell.label.text = @"Ingredient"; cell.textField.text = ingredient.name; cell.textField.placeholder = @"Name"; } else if (indexPath.row == 1) { cell.label.text = @"Amount"; cell.textField.text = ingredient.amount; cell.textField.placeholder = @"Amount"; } return cell; } ... - (void)save:(id)sender { NSManagedObjectContext *context = [recipe managedObjectContext]; /* Если объект ингредиента не создан - создаем и конфигурируем его */ if (!ingredient) { self.ingredient = [NSEntityDescription insertNewObjectForEntityForName:@"Ingredient" inManagedObjectContext:context]; [recipe addIngredientsObject:ingredient]; ingredient.displayOrder = [NSNumber numberWithInteger:[recipe.ingredients count]]; } /* Обновляем объект ингредиента значениями из текстовых полей */ EditingTableViewCell *cell; cell = (EditingTableViewCell *)[self.tableView cellForRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]]; ingredient.name = cell.textField.text; cell = (EditingTableViewCell *)[self.tableView cellForRowAtIndexPath:[NSIndexPath indexPathForRow:1 inSection:0]]; ingredient.amount = cell.textField.text; /* Сохраняем изменения */ NSError *error = nil; if (![context save:&error]) { NSLog(@"Unresolved error %@, %@", error, [error userInfo]); abort(); } [self.navigationController popViewControllerAnimated:YES]; } ... @end
Форма добавления ингредиента выглядит следующим образом

При нажатии на кнопку Save ингредиент будет сохранен в базе данных и отображен в списке. При нажатии на кнопку Cancel новый ингредиент сохранен не будет.
How To Remove an Existing Recipe
Контроллер RecipeListTableViewController содержит следующий код для поддержки удаления рецепта
- (void)viewDidLoad { ... //Создаем кнопку Edit self.navigationItem.leftBarButtonItem = self.editButtonItem; ... } ... // Включаем поддержку редактирования в UITableView - (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath { if (editingStyle == UITableViewCellEditingStyleDelete) { // Удалем выделенный рецепт NSManagedObjectContext *context = [fetchedResultsController managedObjectContext]; [context deleteObject:[fetchedResultsController objectAtIndexPath:indexPath]]; // Сохраняем изменения NSError *error; if (![context save:&error]) { NSLog(@"Unresolved error %@, %@", error, [error userInfo]); abort(); } } }
Теперь, при нажатии на кнопку Edit или выполнение жеста над UITableViewCell, UITableView перейдет в режим редактирования, что позволит вам удалить рецепт (см. скриншоты ниже).


При удалении рецепта, все связанные объекты (ингредиенты и картинка) в базе данных будут также удалены.
How To Remove an Existing Ingredient
В контроллере RecipeDetailViewController, все устроено аналогичным образом, за исключением добавления картинки (рассмотрено не будет).
- (UITableViewCellEditingStyle)tableView:(UITableView *)tableView editingStyleForRowAtIndexPath:(NSIndexPath *)indexPath { UITableViewCellEditingStyle style = UITableViewCellEditingStyleNone; // Only allow editing in the ingredients section. // В секции отображения ингредиентов, последняя строка добавляется автоматически (см. tableView:cellForRowAtIndexPath:), она требуется для добавления нового ингредиента, таким образом в списке ингредиентов включаем стиль удаления, а в последнем элементе стиль добавления if (indexPath.section == INGREDIENTS_SECTION) { if (indexPath.row == [recipe.ingredients count]) { style = UITableViewCellEditingStyleInsert; } else { style = UITableViewCellEditingStyleDelete; } } return style; } - (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath { // Только удаление и только в списке ингредиентов if ((editingStyle == UITableViewCellEditingStyleDelete) && (indexPath.section == INGREDIENTS_SECTION)) { // Удаляем ингредиент Ingredient *ingredient = [ingredients objectAtIndex:indexPath.row]; [recipe removeIngredientsObject:ingredient]; [ingredients removeObject:ingredient]; NSManagedObjectContext *context = ingredient.managedObjectContext; [context deleteObject:ingredient]; [self.tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationTop]; } }
Форма добавления и удаления ингредиентов выглядит следующим образом

Conclusion
В данной статье, я специально не рассматривал создание картинки для рецепта — предполагая самостоятельное изучение читателем, данной функциональности (см. метод photoTapped контроллера RecipeDetailViewController). Приведенные примеры исходного кода, являются выдержками, изучить исходный код полностью, можно скачав проект (см. раздел References). Я надеюсь, что проделанная мной работа, по написанию этой серии статей, оказалась полезна для тебя — дорогой читатель! Всем спасибо за внимание и за проявленое терпение.
