Пользователь
0,0
рейтинг
16 января 2012 в 12:58

Разработка → Разбираем iPhone Core Data Recipes. Часть 1 из песочницы

Introduction


Цель данной статьи — помочь начинающему iOS разработчику, понять, как правильно работать с SQLite базой данных используя Core Data на примере iPhone Core Data Recipes. В первой части из серии статей, будет рассмотрено взаимодействие приложения и базы данных, а также работа со связанными записями (Relationships).

Prerequisites


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


Данный набор позволит вам просмотреть, изменить и запустить приложение на симуляторе. В случае же, если вы захотите попробовать запустить его на настоящем iPhone, требуется участие в iOS Developer Program.

А также, что немало важно, нужно базовое понимание структуры языка Objective-C и приложения.

Ссылки на используемые материалы и инструменты предоставлены в разделе References.

Что такое iPhone Core Data Recipes?

Xcode проект от разработчиков из Apple, который дает общее представление о том, как использовать view controllers, table views и Core Data в iPhone приложениях. Конечно, данный проект также актуален для iPad, но в него необходимо внести интерфейсные изменения, для корректного отображения на iPad.

iPhone Core Data Recipes Screenshots

Скриншоты ниже, дают общее представление о интерфейсе приложения. В главном окне «Recipes» — оторбаражается список готовых продуктов. Нажав на один из них, приложение отображает информацию о том, как приготовить данный продукт, его категорию, сколько на это нужно времени, какие и в каком количестве необходимы ингредиенты, а также кнопку которая отображает текстовую инструкцию по приготовлению (скриншот не приведен). Также в приложении есть возмоность конвертации значений между граммами, фунтами и унциями, а также таблица соответствия температур (по цельсию и фаренгейту).



Database Structure


Общее представление о структуре базы данных можно получить со скриншота ниже:

Более делально структуру можно изучить открыв в Xcode файл Recipes.xcdatamodel.

Описание таблиц

Таблица Recipe содержит следующие атрибуты:
  • instructions типа String
  • name типа String
  • overview типа String
  • prepTime типа String
  • thumbnailImage типа Transformable

Таблица Ingredient содержит следующие атрибуты:
  • amount типа String
  • displayOrder типа Integer 16
  • name типа String

Таблица RecipeType содержит следующие атрибуты:
  • name типа String

Таблица Image содержит следующие атрибуты:
  • image типа Transformable

Я думаю со стандартными типами все понятно. А вот, что за тип такой Transformable? Core Data поддерживает целый ряд стандартных типов, например строка, дата или число. Но иногда необходимо создать атрибут, значение которого не поддерживается напрямую. Например хранить картинку или цвет, которые являются экземплярами классов NSImage и NSColor. Для этого и нужен тип Transformable, детальное описание этого типа, вы можете прочитать на сайте iOS Developer Library в разделе Non-Standard Persistent Attributes (см. ссылку в разделе References).

Связи между таблицами (Relationships)

В структуре базы данных, присутствует главная таблица Recipe, которая имеет связи различных типов, с тремя другими. Например, таблица Recipe имеет связь один-ко-многим с таблицой Ingredient. Тип и направление связи, можно понять по стрелкам и их количеству. Например, таблица Recipe имеет связь один-к-одному с таблицей RecipeType, но при этом таблица RecipeType имеет связь один-ко-многим с таблицой Recipe, т.е. связь двусторонняя не одного типа. Это означает, что запись из таблицы Recipe, может быть связанна только с одной записью из таблицы RecipeType, но при этом запись из таблицы RecipeType может быть связана со многими записями в таблице Recipe. В данном конкретном примере, выражаясь простыми словами, можно сказать, что продукт может быть только в одной категории, при этом разные продукты, также могут быть в этой же категории, например в категории Dessert (Десерт).

Core Data автоматически управляет связями, т.е. вам нужно только правильно их построить в вашей модели.

Тип связи задается в свойствах Relationship в Data Model Inspector (см. пример ниже).

При проектировании структуры вашей базы данных, важно не забывать про Delete Rule в свойствах связи. В данном примере, при удалении продукта удалятся и все его ингредиенты.

Классы

После того, как вы завершили проектирование базы данных, необходимо сгенерировать Objective-C классы для вашей модели. В нашем случае мы имеем классы для таблиц Recipe и Ingredient.

Recipe.h
@interface ImageToDataTransformer : NSValueTransformer {
}
@end

@interface Recipe : NSManagedObject {
}

@property (nonatomic, retain) NSString *instructions;
@property (nonatomic, retain) NSString *name;
@property (nonatomic, retain) NSString *overview;
@property (nonatomic, retain) NSString *prepTime;
@property (nonatomic, retain) NSSet *ingredients;
@property (nonatomic, retain) UIImage *thumbnailImage;

@property (nonatomic, retain) NSManagedObject *image;
@property (nonatomic, retain) NSManagedObject *type;

@end

@interface Recipe (CoreDataGeneratedAccessors)
- (void)addIngredientsObject:(NSManagedObject *)value;
- (void)removeIngredientsObject:(NSManagedObject *)value;
- (void)addIngredients:(NSSet *)value;
- (void)removeIngredients:(NSSet *)value;
@end


Ingredient.h
@class Recipe;
@interface Ingredient : NSManagedObject {
}

@property (nonatomic, retain) NSString *name;
@property (nonatomic, retain) NSString *amount;
@property (nonatomic, retain) Recipe *recipe;
@property (nonatomic, retain) NSNumber *displayOrder;

@end

Source Code


Приступаем к изучению исходного когда взаимодействия приложения и базы данных.

Файл RecipesAppDelegate.h содержит объявления свойств NSManagedObjectModel, NSManagedObjectContext и NSPersistentStoreCoordinator необходимых для работы с базой данных:
@class RecipeListTableViewController;
@interface RecipesAppDelegate : NSObject <UIApplicationDelegate> {
    NSManagedObjectModel *managedObjectModel;
    NSManagedObjectContext *managedObjectContext;	    
    NSPersistentStoreCoordinator *persistentStoreCoordinator;
}
@property (nonatomic, retain, readonly) NSManagedObjectModel *managedObjectModel;
@property (nonatomic, retain, readonly) NSManagedObjectContext *managedObjectContext;
@property (nonatomic, retain, readonly) NSPersistentStoreCoordinator *persistentStoreCoordinator;

- (NSString *)applicationDocumentsDirectory;

@end


Файл RecipesAppDelegate.m управляет этими свойствами, а также следит за тем, чтобы данные были целостны

...
- (void)applicationDidFinishLaunching:(UIApplication *)application {
    recipeListController.managedObjectContext = self.managedObjectContext;
 ...
}
//Проверяем были ли изменения, если да, сохраняем перед закрытием
- (void)applicationWillTerminate:(UIApplication *)application {
    NSError *error;
    if (managedObjectContext != nil) {
        if ([managedObjectContext hasChanges] && ![managedObjectContext save:&error]) {
			NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
			abort();
        } 
    }
}
//Создаем объект NSManagedObjectContext, если он еще не создан
- (NSManagedObjectContext *)managedObjectContext {
	
    if (managedObjectContext != nil) {
        return managedObjectContext;
    }
	
    NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
    if (coordinator != nil) {
        managedObjectContext = [NSManagedObjectContext new];
        [managedObjectContext setPersistentStoreCoordinator: coordinator];
    }
    return managedObjectContext;
}
//Создаем объект NSManagedObjectModel, если он еще не создан
- (NSManagedObjectModel *)managedObjectModel {
	
    if (managedObjectModel != nil) {
        return managedObjectModel;
    }
    managedObjectModel = [[NSManagedObjectModel mergedModelFromBundles:nil] retain];    
    return managedObjectModel;
}
//Создаем и возвращаем NSPersistentStoreCoordinator, если он еще не создан. NSPersistentStoreCoordinator содержит указатель на файл базы данных
- (NSPersistentStoreCoordinator *)persistentStoreCoordinator {
	
    if (persistentStoreCoordinator != nil) {
        return persistentStoreCoordinator;
    }
		
	NSString *storePath = [[self applicationDocumentsDirectory] stringByAppendingPathComponent:@"Recipes.sqlite"];
	/*
	 Если файл базы данных уже существует открываем его, если нет, переносим его из нашего проекта. Файл из проекта уже содержит в себе данные.
	 */
	NSFileManager *fileManager = [NSFileManager defaultManager];
	if (![fileManager fileExistsAtPath:storePath]) {
		NSString *defaultStorePath = [[NSBundle mainBundle] pathForResource:@"Recipes" ofType:@"sqlite"];
		if (defaultStorePath) {
			[fileManager copyItemAtPath:defaultStorePath toPath:storePath error:NULL];
		}
	}
	
	NSURL *storeUrl = [NSURL fileURLWithPath:storePath];
	
	NSError *error;
    persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel: [self managedObjectModel]];
    if (![persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeUrl options:nil error:&error]) {
		NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
		abort();
    }    
		
    return persistentStoreCoordinator;
}
//Получаем путь до директории Documents приложения
- (NSString *)applicationDocumentsDirectory {
	return [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject];
}
...


Теперь, нам необходимо отобразить список продуктов в главном окне приложения. Файл RecipeListTableViewController.m (интерфейс RecipeListTableViewController) содержит необходимый код

...
//Получаем все записи из таблицы Recipe
- (NSFetchedResultsController *)fetchedResultsController {
    if (fetchedResultsController == nil) {
        NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
        NSEntityDescription *entity = [NSEntityDescription entityForName:@"Recipe" inManagedObjectContext:managedObjectContext];
        [fetchRequest setEntity:entity];
        
        // Сортируем их по имени
        NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"name" ascending:YES];
        NSArray *sortDescriptors = [[NSArray alloc] initWithObjects:sortDescriptor, nil];
        
        [fetchRequest setSortDescriptors:sortDescriptors];
        
        NSFetchedResultsController *aFetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:managedObjectContext sectionNameKeyPath:nil cacheName:@"Root"];
        aFetchedResultsController.delegate = self;
        self.fetchedResultsController = aFetchedResultsController;
        
        [aFetchedResultsController release];
        [fetchRequest release];
        [sortDescriptor release];
        [sortDescriptors release];
    }
	
	return fetchedResultsController;
} 
...
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    // Создаем RecipeTableViewCell и помещаем его в текущую ячейку. RecipeTableViewCell это унаследованный и измененный класс UITableViewCell
    static NSString *RecipeCellIdentifier = @"RecipeCellIdentifier";
    
    RecipeTableViewCell *recipeCell = (RecipeTableViewCell *)[tableView dequeueReusableCellWithIdentifier:RecipeCellIdentifier];
    if (recipeCell == nil) {
        recipeCell = [[[RecipeTableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:RecipeCellIdentifier] autorelease];
		recipeCell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
    }
    
	[self configureCell:recipeCell atIndexPath:indexPath];
    
    return recipeCell;
}

- (void)configureCell:(RecipeTableViewCell *)cell atIndexPath:(NSIndexPath *)indexPath {
    // Конфигурирем ячейку
	Recipe *recipe = (Recipe *)[fetchedResultsController objectAtIndexPath:indexPath];
    cell.recipe = recipe;
}
...

Все, данного кода достаточно для отображения списка продуктов в главном окне. Конечно, не стоит забывать, что здесь я привел не весь код, а только выдержки из него. Например, открыв файл RecipeListTableViewController.m вы можете изучить то, как приложение следит за изменениями (didChangeSection, didChangeObject, controllerWillChangeContent, commitEditingStyle и т.д.).

Приступим к отображению списка ингредиентов. Для этого у нас есть интерфейс RecipeDetailViewController, у которого, в свою очередь, есть следующие свойства


Recipe *recipe;
NSMutableArray *ingredients;

@property (nonatomic, retain) Recipe *recipe;
@property (nonatomic, retain) NSMutableArray *ingredients;


Передача текущего рецепта из интерфейса RecipeListTableViewController в интерфейс RecipeDetailViewController

...
//Пользователь нажал на рецепт
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
	Recipe *recipe = (Recipe *)[fetchedResultsController objectAtIndexPath:indexPath];
    
    [self showRecipe:recipe animated: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];
}
...


Оторбражаем рецепт

- (void)viewWillAppear:(BOOL)animated {
    
    [super viewWillAppear:animated];
	
    [photoButton setImage:recipe.thumbnailImage forState:UIControlStateNormal];
	self.navigationItem.title = recipe.name;
    nameTextField.text = recipe.name;    
    overviewTextField.text = recipe.overview;    
    prepTimeTextField.text = recipe.prepTime;    
	[self updatePhotoButton];
//Сортируем ингредиенты
	NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"displayOrder" ascending:YES];
	NSArray *sortDescriptors = [[NSArray alloc] initWithObjects:&sortDescriptor count:1];
	
	NSMutableArray *sortedIngredients = [[NSMutableArray alloc] initWithArray:[recipe.ingredients allObjects]];
	[sortedIngredients sortUsingDescriptors:sortDescriptors];
	self.ingredients = sortedIngredients;

	[sortDescriptor release];
	[sortDescriptors release];
	[sortedIngredients release];
    [self.tableView reloadData]; 
}


В tableView cellForRowAtIndexPath создаем ячейку для отображения рецепта

static NSString *IngredientsCellIdentifier = @"IngredientsCell";
cell = [tableView dequeueReusableCellWithIdentifier:IngredientsCellIdentifier];
if (cell == nil) {
	cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:IngredientsCellIdentifier] autorelease];
	cell.accessoryType = UITableViewCellAccessoryNone;
}

Все, данного кода достаточно для отображения списка ингредиентов. Смысл его в том, что из главного окна, мы передаем объект Recipe, окну которое отображает список ингредиентов. При этом, чтобы получить список ингредиентов из базы данных, не требуется создавать новый NSEntityDescription, достаточно просто обратиться в свойству recipe.ingredients и Core Data автоматически вернет необходимые ингредиенты из базы данных.

Conclusion


Конечно данная статья является не полным описанием этого проекта. Если вам, дорогие читатели, будет интересно, я готов продолжить серию статей и например в следующей, описать, как в данном проекте добавляются, удаляются и изменяются объекты в базе данных. Всем спасибо за внимание и за проявленое терпение.

References


Alex L. @alexal
карма
3,0
рейтинг 0,0
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама

Самое читаемое Разработка

Комментарии (15)

  • 0
    Спасибо :)

    PS. <глупый_вопрос>А зачем Image хранить в CoreData?</глупый_вопрос>
    • 0
      Пожалуйста. Вам спасибо за комментарий.

      P.S.: А чем вас смущает хранение картинок в базе данных? Интересно ли вам почитать продолжение?
      • +1
        Продолжение весьма приветствуется :)

        PS.
        Не быстрее ли их с «диска» грузить, чем через sqlite?
        Или у CoreData оно как раз будет автоматически мапится на «диск»?
        • 0
          Спасибо, продолжение будет.

          Да, конечно, чтение картинок с диска будет быстрее, например вот так:

          [UIImage imageWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"image.png"]]
          


          Но, в данном конкретном примере, разработчики из Apple, решили хранить их в базе данных.
  • 0
    Как вовремя и в карту! Еще утром гуглил заграничный интернет на предмет ios5 sqlite :)
    • 0
      Спасибо за комментарий. Интересно ли продолжение?
    • 0
      Важно заметить, что Core Data не sqlite. Это фреймворк которые позволяет EAV данные сериализовать в sqlite, равно как и в xml, бинарные plist, память, ну или даже в ваш собственный формат. Это довольно обширная область с множеством своих подводных камней и нюансов которую не рекомендуется рассматривать именно как замену БД.
      • 0
        Поправлюсь: не рекомендуется рассматривать как полную замену БД. Зачастую Core Data начинают применять к месту и не месту, а часто и не правильно (рассматривая ее исключительно как обертку над как раз sqlite), отсюда много ошибок, повышенное требование к ресурсам.
  • 0
    да, конечно
  • +1
    Тоже жду продолжения.
    • 0
      Спасибо за комментарий. Продолжение будет.
  • 0
    в помощь github.com/magicalpanda/MagicalRecord

    Сводит количество кода при работе с Core Data к минимуму.
    • 0
      Спасибо!
  • 0
    Есть простое приложение, которое использует sqlite.
    Есть необходимость добавить работу с iCloud.
    Насколько я знаю, Core Data дружит с iCloud.

    Пробовал ли кто работать с Core Data в iCloud?
    Действительно ли там всё удобно и просто?
    Стоит ли переходить из-за этого с sqlite на Core Data?
  • 0
    Спасибо за статью, но с пунктуацией у вас полный п.

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