Pro Core Data for iOS. Глава №1. Практическая часть

от автора

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

image

Содержание:

  • Глава №1. Приступаем (Практическая часть)
  • Глава №2. Усваиваем Core Data
  • Глава №3. Хранение данных: SQLite и другие варианты
  • Глава №4. Создание модели данных
  • Глава №5. Работаем с объектами данных
  • Глава №6. Обработка результатирующих множеств
  • Глава №7. Настройка производительности и используемой памяти
  • Глава №8. Управление версиями и миграции
  • Глава №9. Управление таблицами с использованием NSFetchedResultsController
  • Глава №10. Использование Core Data в продвинутых приложениях

Практическая часть

Так как это первая глава и её можно считать вводной, то в качестве практического задания мы выберем создание обычного социального приложения, которое будет отображать список наших друзей из ВК и использовать Core Data для хранения данных о них.
Примерно (в процессе решим что добавить/исключить) таким образом будет выглядеть наше приложение после нескольких часов (а может и минут) упорного программирования:

image image

Как Вы могли уже догадаться, использовать мы будем Vkontakte iOS SDK v2.0.
Кстати, прошу меня простить за то, что в практической части будет использоваться не только XCode, но и AppCode (ребятам из JB спасибо за продукт!). Всё, что можно сделать в AppCode, будет там сделано.

Поехали…

Создание пустого проекта

Создадим пустой проект без Core Data — Single View Application.
image
image

Приложение удачно запустилось:
image

Добавление и настройка UITableView

Открываем ASAViewController.h и добавляем следующее свойство:

@property (nonatomic, strong) UITableView *tableView; 

Полный вид ASAViewController.h:

#import <UIKit/UIKit.h>  @interface ASAViewController : UIViewController  @property (nonatomic, strong) UITableView *tableView;  @end 

Открываем ASAViewController.m и в метод viewDidLoad добавляем строки создания таблицы UITableView:

    CGRect frame = [[UIScreen mainScreen] bounds];     _tableView = [[UITableView alloc]                                initWithFrame:frame                                        style:UITableViewStylePlain];     [self.view addSubview:_tableView]; 

Полный вид ASAViewController.m:

#import "ASAViewController.h"  @implementation ASAViewController  - (void)viewDidLoad {     CGRect frame = [[UIScreen mainScreen] bounds];     _tableView = [[UITableView alloc]                                initWithFrame:frame                                        style:UITableViewStylePlain];     [_tableView setDelegate:self];     [_tableView setDataSource:self];     [self.view addSubview:_tableView]; }  @end 

Запускаем:
image

Осталось реализовать методы делегатов UITableViewDelegate и UITableViewDataSource.
Дописываем протоколы в  ASAViewController.h:

@interface ASAViewController : UIViewController <UITableViewDataSource, UITableViewDelegate> 

Открываем ASAViewController.m и реализовываем два метода (один для возврата кол-ва друзей в списке, а второй для создания заполненной ячейки с данными пользователя):

#pragma mark - UITableViewDelegate & UITableViewDataSource  - (NSInteger)tableView:(UITableView *)tableView   numberOfRowsInSection:(NSInteger)section {     return [_userFriends count]; }  - (UITableViewCell *)tableView:(UITableView *)tableView   cellForRowAtIndexPath:(NSIndexPath *)indexPath {     static NSString *cellID = @"friendID";      UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellID];     if(nil == cell){         cell = [[UITableViewCell alloc]                 initWithStyle:UITableViewCellStyleSubtitle               reuseIdentifier:cellID];     }      //    setting default image while main photo is loading     cell.imageView.image = [UIImage imageNamed:@"default.png"];      dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{         NSString* imgPath = _userFriends[(NSUInteger)indexPath.row][@"photo"];         NSData* img = [NSData dataWithContentsOfURL:[NSURL URLWithString:imgPath]];          dispatch_async(dispatch_get_main_queue(), ^{             cell.imageView.image = [UIImage imageWithData:img];         });     });      NSString* firstName = _userFriends[(NSUInteger)indexPath.row][@"first_name"];     NSString* lastName = _userFriends[(NSUInteger)indexPath.row][@"last_name"];     NSString* fullName = [NSString stringWithFormat:@"%@ %@", firstName, lastName];     cell.textLabel.text = fullName;      NSString* status = _userFriends[(NSUInteger)indexPath.row][@"status"];     cell.detailTextLabel.text = status;      return cell; } 

Переменная _userFriends является свойством ASAViewController:

@property (nonatomic, strong) NSMutableArray *userFriends; 

Итоговый вид ASAViewController.h и ASAViewController.m:

#import <UIKit/UIKit.h>  @interface ASAViewController : UIViewController <UITableViewDataSource, UITableViewDelegate>  @property (nonatomic, strong) UITableView *tableView; @property (nonatomic, strong) NSMutableArray *userFriends;  @end 

#import "ASAViewController.h"  @implementation ASAViewController  - (void)viewDidLoad {     _userFriends = [[NSMutableArray alloc] init];      CGRect frame = [[UIScreen mainScreen] bounds];     _tableView = [[UITableView alloc]                                initWithFrame:frame                                        style:UITableViewStylePlain];     [_tableView setDelegate:self];     [_tableView setDataSource:self];     [self.view addSubview:_tableView]; }  #pragma mark - UITableViewDelegate & UITableViewDataSource  - (NSInteger)tableView:(UITableView *)tableView   numberOfRowsInSection:(NSInteger)section {     return [_userFriends count]; }  - (UITableViewCell *)tableView:(UITableView *)tableView   cellForRowAtIndexPath:(NSIndexPath *)indexPath {     static NSString *cellID = @"friendID";      UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellID];     if(nil == cell){         cell = [[UITableViewCell alloc]                 initWithStyle:UITableViewCellStyleSubtitle               reuseIdentifier:cellID];     }      //    setting default image while main photo is loading     cell.imageView.image = [UIImage imageNamed:@"default.png"];      dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{         NSString* imgPath = _userFriends[(NSUInteger)indexPath.row][@"photo"];         NSData* img = [NSData dataWithContentsOfURL:[NSURL URLWithString:imgPath]];          dispatch_async(dispatch_get_main_queue(), ^{             cell.imageView.image = [UIImage imageWithData:img];         });     });      NSString* firstName = _userFriends[(NSUInteger)indexPath.row][@"first_name"];     NSString* lastName = _userFriends[(NSUInteger)indexPath.row][@"last_name"];     NSString* fullName = [NSString stringWithFormat:@"%@ %@", firstName, lastName];     cell.textLabel.text = fullName;      NSString* status = _userFriends[(NSUInteger)indexPath.row][@"status"];     cell.detailTextLabel.text = status;      return cell; }  @end 

Всё должно запускаться на ура. Переходим к следующему шагу.

Интегрирование ВКонтакте iOS SDK v2.0

Забираем исходники по этой ссылке.

Подключаем QuartzCore.framework
image

Добавляем Vkontakte iOS SDK
image

В ASAAppDelegate.h добавляем два протокола:

@interface ASAAppDelegate : UIResponder <UIApplicationDelegate, VKConnectorDelegate, VKRequestDelegate> 

Открываем файл реализации ASAAppDelegate.m и вставляем следующие строки в метод - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions:

    [[VKConnector sharedInstance]                   setDelegate:self];     [[VKConnector sharedInstance] startWithAppID:@"3541027"                                       permissons:@[@"friends"]]; 

Данный код при запуске приложения покажет всплывающее окно пользователю для авторизации в социальной сети ВКонтакте.
image

В  ASAAppDelegate.m реализуем еще два метода:

#pragma mark - VKConnectorDelegate  - (void)        VKConnector:(VKConnector *)connector accessTokenRenewalSucceeded:(VKAccessToken *)accessToken { //   now we can make request     [[VKUser currentUser] setDelegate:self];     [[VKUser currentUser] friendsGet:@{             @"uid"    : @([VKUser currentUser].accessToken.userID),             @"fields" : @"first_name,last_name,photo,status"     }]; }  #pragma mark - VKRequestDelegate  - (void)VKRequest:(VKRequest *)request          response:(id)response {     ASAViewController *controller = (ASAViewController *)self.window.rootViewController;      controller.userFriends = response[@"response"];     [controller.tableView reloadData]; } 

Окончательный вид ASAAppDelegate.h и ASAAppDelegate.m на данном этапе:

#import <UIKit/UIKit.h> #import "VKConnector.h" #import "VKRequest.h"  @class ASAViewController;  @interface ASAAppDelegate : UIResponder <UIApplicationDelegate, VKConnectorDelegate, VKRequestDelegate>  @property (strong, nonatomic) UIWindow *window; @property (strong, nonatomic) ASAViewController *viewController;  @end 

#import "ASAAppDelegate.h" #import "ASAViewController.h" #import "VKUser.h" #import "VKAccessToken.h"   @implementation ASAAppDelegate  - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {     self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];      // Override point for customization after application launch.     self.viewController = [[ASAViewController alloc] initWithNibName:@"ASAViewController" bundle:nil];     self.window.rootViewController = self.viewController;     [self.window makeKeyAndVisible];      [[VKConnector sharedInstance]                   setDelegate:self];     [[VKConnector sharedInstance] startWithAppID:@"3541027"                                       permissons:@[@"friends"]];      return YES; }  #pragma mark - VKConnectorDelegate  - (void)        VKConnector:(VKConnector *)connector accessTokenRenewalSucceeded:(VKAccessToken *)accessToken { //   now we can make request     [[VKUser currentUser] setDelegate:self];     [[VKUser currentUser] friendsGet:@{             @"uid"    : @([VKUser currentUser].accessToken.userID),             @"fields" : @"first_name,last_name,photo,status"     }]; }  #pragma mark - VKRequestDelegate  - (void)VKRequest:(VKRequest *)request          response:(id)response {     ASAViewController *controller = (ASAViewController *)self.window.rootViewController;      controller.userFriends = response[@"response"];     [controller.tableView reloadData]; }  @end 

Запускаем приложение и видим примерно следующее (не забывайте, что в указанном выше примере не используется кэширование запросов намеренно):
image
image

Десерт из Core Data

Вот мы и подошли к самому интересному и увлекательному! Надеюсь Вы еще не потеряли желание доделать практическую часть 😉 Отвлекитесь, выпейте чайку с сушками, погрызите конфетку, разомнитесь, подтянитесь.

Зачем нам здесь Core Data? Мы поступим следующим образом: при первом запросе к серверу ВКонтакте мы получим список друзей и запрашиваемые поля (статус, фотография, имя, фамилия), эту информацию сохраним в локальном хранилище используя Core Data, а потом запустим приложение и во время запроса отключим интернет и выведем список друзей пользователя, которые были сохранены локально во время первого запроса. Идёт? Тогда приступим.

Для обработки факта отсутствия интернет соединения мы воспользуемся следующим методом из протокола VKRequestDelegate:

- (void)VKRequest:(VKRequest *)request         connectionErrorOccured:(NSError *)error { //    TODO } 

Тело метода мы напишем немного позже.

Ах да, совсем забыл! Подключаем  CoreData.framework.
image
Добавляем три любимые нами свойства в ASAAppDelegate.h:

@property (nonatomic, strong) NSManagedObjectModel *managedObjectModel; @property (nonatomic, strong) NSPersistentStoreCoordinator *coordinator; @property (nonatomic, strong) NSManagedObjectContext *managedObjectContext; 

Теперь переходим в ASAAppDelegate.m для того, чтобы реализовать явные геттеры для всех трёх свойств.
Managed Object Model:

- (NSManagedObjectModel *)managedObjectModel {     if(nil != _managedObjectModel)         return _managedObjectModel;      _managedObjectModel = [NSManagedObjectModel mergedModelFromBundles:nil];          return _managedObjectModel; } 

Persistent Store Coordinator:

- (NSPersistentStoreCoordinator *)coordinator {     if(nil != _coordinator)         return _coordinator;      NSURL *storeURL = [[[[NSFileManager defaultManager]                                         URLsForDirectory:NSDocumentDirectory                                                inDomains:NSUserDomainMask]                                         lastObject]                                         URLByAppendingPathComponent:@"BasicApplication.sqlite"];      _coordinator = [[NSPersistentStoreCoordinator alloc]                                                   initWithManagedObjectModel:self.managedObjectModel];      NSError *error = nil;     if(![_coordinator addPersistentStoreWithType:NSSQLiteStoreType                                    configuration:nil                                              URL:storeURL                                          options:nil                                            error:&error]){         NSLog(@"Unresolved error %@, %@", error, [error userInfo]);         abort();     }      return _coordinator; } 

Managed Object Context:

- (NSManagedObjectContext *)managedObjectContext {     if(nil != _managedObjectContext)         return _managedObjectContext;      NSPersistentStoreCoordinator *storeCoordinator = self.coordinator;      if(nil != storeCoordinator){         _managedObjectContext = [[NSManagedObjectContext alloc] init];         [_managedObjectContext setPersistentStoreCoordinator:storeCoordinator];     }      return _managedObjectContext; } 

Build… И… и… всё нормально.

Теперь переходим к созданию модели. Кстати, хочу отметить, что я делаю всё без страховки и, может быть в конце что-то с чем-то и не состыкуется, но мы же смелые программисты!
Для создания модели нам понадобиться тот самый XCode.
Открываем наш проект в нём, нажимаем Control+N и выбираем Core Data -> Data Model:
image

Сохраним модель под названием Friend:
image

Видим уже довольно знакомый экран:
image

Создадим новую сущность под названием Friend и добавим 4 свойства: last_name (String), first_name (String), status (String), photo (Binary Data).
image

Завершаем и закрываем XCode.

Следующее, что мы должны сделать, так это сохранить данные о пользователях после осуществления запроса.
Открываем ASAAppDelegate.m, спускаемся к метод VKRequest:response: и изменяем его следующим образом:

- (void)VKRequest:(VKRequest *)request          response:(id)response {     ASAViewController *controller = (ASAViewController *)self.window.rootViewController;      controller.userFriends = response[@"response"];     [controller.tableView reloadData];  //    сохраняем данные в фоне, чтобы не замораживать интерфейс     dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{         for(NSDictionary *user in controller.userFriends){             NSManagedObject *friend = [NSEntityDescription insertNewObjectForEntityForName:@"Friend"                                                                     inManagedObjectContext:self.managedObjectContext];              [friend setValue:user[@"first_name"] forKey:@"first_name"];             [friend setValue:user[@"last_name"] forKey:@"last_name"];             [friend setValue:[NSData dataWithContentsOfURL:[NSURL URLWithString:user[@"photo"]]] forKey:@"photo"];             [friend setValue:user[@"status"] forKey:@"status"];              NSLog(@"friend: %@", friend);         }          if([self.managedObjectContext hasChanges] && ![self.managedObjectContext save:nil]){             NSLog(@"Unresolved error!");             abort();         }     }); } 

На каждой итерации мы создаём новый объект, устанавливаем его поля и сохраняем. В консоли можете наблюдать радующие глаз строки:
image

Такс, осталось доработать отображение таблицы при обрыве интернет соединения. Весь код пойдёт в метод - (void)VKRequest:(VKRequest *)request connectionErrorOccured:(NSError *)error и будет выглядеть следующим образом:

- (void)VKRequest:(VKRequest *)request         connectionErrorOccured:(NSError *)error { //    понадобится нам для хранения словарей с пользовательской информацией     NSMutableArray *data = [[NSMutableArray alloc] init];  //    конфигурируем запрос на получение друзей     NSFetchRequest *fetchRequest = [[NSFetchRequest alloc]                                                     initWithEntityName:@"Friend"];     NSSortDescriptor *sortDescriptor = [NSSortDescriptor sortDescriptorWithKey:@"last_name"                                                                      ascending:YES];     [fetchRequest setSortDescriptors:@[sortDescriptor]];  //    осуществляем запрос     NSArray *tmpData = [self.managedObjectContext executeFetchRequest:fetchRequest                                                                 error:nil];  //    обрабатываем запрос     for(NSManagedObject *object in tmpData){ //        эта строка здесь потому, что у меня в друзьях есть удаленный пользователь - мудак :)         if([object valueForKey:@"status"] == nil)             continue;          NSDictionary *tmp = @{                 @"last_name": [object valueForKey:@"first_name"],                 @"first_name": [object valueForKey:@"last_name"],                 @"photo": [object valueForKey:@"photo"],                 @"status": [object valueForKey:@"status"]         };          [data addObject:tmp];     }  //    теперь данные "перебросим" в нужный контроллер     ASAViewController *controller = (ASAViewController *)self.window.rootViewController;     controller.userFriends = data;     [controller.tableView reloadData]; } 

И небольшие коррективы внести надо в метод - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath:

- (UITableViewCell *)tableView:(UITableView *)tableView   cellForRowAtIndexPath:(NSIndexPath *)indexPath {     static NSString *cellID = @"friendID";      UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellID];     if(nil == cell){         cell = [[UITableViewCell alloc]                 initWithStyle:UITableViewCellStyleSubtitle               reuseIdentifier:cellID];     }      //    setting default image while main photo is loading     cell.imageView.image = [UIImage imageNamed:@"default.png"];      dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{         NSData *img;          if([_userFriends[(NSUInteger) indexPath.row][@"photo"] isKindOfClass:[NSData class]]){             img = _userFriends[(NSUInteger) indexPath.row][@"photo"];         } else {             NSString* imgPath = _userFriends[(NSUInteger)indexPath.row][@"photo"];             img = [NSData dataWithContentsOfURL:[NSURL URLWithString:imgPath]];         }          dispatch_async(dispatch_get_main_queue(), ^{             cell.imageView.image = [UIImage imageWithData:img];         });     });      NSString* firstName = _userFriends[(NSUInteger)indexPath.row][@"first_name"];     NSString* lastName = _userFriends[(NSUInteger)indexPath.row][@"last_name"];     NSString* fullName = [NSString stringWithFormat:@"%@ %@", firstName, lastName];     cell.textLabel.text = fullName;      NSString* status = _userFriends[(NSUInteger)indexPath.row][@"status"];     cell.detailTextLabel.text = status;      return cell; } 

Ура! Приложение завершено и выводит оно друзей из локального хранилища:
image

Слёзы радости

Наконец-то мы закончили нашу первую, но не последнюю практическую часть. Весь проект Вы можете найти по этой ссылке (он в архиве).

Надеюсь, что спина и пальцы не устали.
Надеюсь, что Вы довольны проведенным временем в компании c Core Data.
Надеюсь, что Вы хотите видеть продолжения.

Примечание

Ничто не может радовать автора, как оставленный комментарий, даже если это критика 😉

Благодарю за внимание!

ссылка на оригинал статьи http://habrahabr.ru/post/191472/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *