Сегодня хочу начать вольный перевод книги Михаеля Привата и Роберта Варнера «Pro Core Data for iOS», которую можете скачать по этой ссылке. Каждая глава будет содержать теоретическую и практическую часть.
Содержание:
- Глава №1. Приступаем (Практическая часть)
- Глава №2. Усваиваем Core Data
- Глава №3. Хранение данных: SQLite и другие варианты
- Глава №4. Создание модели данных
- Глава №5. Работаем с объектами данных
- Глава №6. Обработка результатирующих множеств
- Глава №7. Настройка производительности и используемой памяти
- Глава №8. Управление версиями и миграции
- Глава №9. Управление таблицами с использованием NSFetchedResultsController
- Глава №10. Использование Core Data в продвинутых приложениях
Практическая часть
Так как это первая глава и её можно считать вводной, то в качестве практического задания мы выберем создание обычного социального приложения, которое будет отображать список наших друзей из ВК и использовать Core Data для хранения данных о них.
Примерно (в процессе решим что добавить/исключить) таким образом будет выглядеть наше приложение после нескольких часов (а может и минут) упорного программирования:
Как Вы могли уже догадаться, использовать мы будем Vkontakte iOS SDK v2.0.
Кстати, прошу меня простить за то, что в практической части будет использоваться не только XCode, но и AppCode (ребятам из JB спасибо за продукт!). Всё, что можно сделать в AppCode, будет там сделано.
Поехали…
Создание пустого проекта
Создадим пустой проект без Core Data — Single View Application.
Приложение удачно запустилось:
Добавление и настройка 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
Запускаем:
Осталось реализовать методы делегатов 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
Добавляем Vkontakte iOS SDK
В 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"]];
Данный код при запуске приложения покажет всплывающее окно пользователю для авторизации в социальной сети ВКонтакте.
В 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
Запускаем приложение и видим примерно следующее (не забывайте, что в указанном выше примере не используется кэширование запросов намеренно):
Десерт из Core Data
Вот мы и подошли к самому интересному и увлекательному! Надеюсь Вы еще не потеряли желание доделать практическую часть 😉 Отвлекитесь, выпейте чайку с сушками, погрызите конфетку, разомнитесь, подтянитесь.
Зачем нам здесь Core Data? Мы поступим следующим образом: при первом запросе к серверу ВКонтакте мы получим список друзей и запрашиваемые поля (статус, фотография, имя, фамилия), эту информацию сохраним в локальном хранилище используя Core Data, а потом запустим приложение и во время запроса отключим интернет и выведем список друзей пользователя, которые были сохранены локально во время первого запроса. Идёт? Тогда приступим.
Для обработки факта отсутствия интернет соединения мы воспользуемся следующим методом из протокола VKRequestDelegate:
- (void)VKRequest:(VKRequest *)request connectionErrorOccured:(NSError *)error { // TODO }
Тело метода мы напишем немного позже.
Ах да, совсем забыл! Подключаем CoreData.framework
.
Добавляем три любимые нами свойства в 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:
Сохраним модель под названием Friend:
Видим уже довольно знакомый экран:
Создадим новую сущность под названием Friend и добавим 4 свойства: last_name (String), first_name (String), status (String), photo (Binary Data).
Завершаем и закрываем 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(); } }); }
На каждой итерации мы создаём новый объект, устанавливаем его поля и сохраняем. В консоли можете наблюдать радующие глаз строки:
Такс, осталось доработать отображение таблицы при обрыве интернет соединения. Весь код пойдёт в метод - (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; }
Ура! Приложение завершено и выводит оно друзей из локального хранилища:
Слёзы радости
Наконец-то мы закончили нашу первую, но не последнюю практическую часть. Весь проект Вы можете найти по этой ссылке (он в архиве).
Надеюсь, что спина и пальцы не устали.
Надеюсь, что Вы довольны проведенным временем в компании c Core Data.
Надеюсь, что Вы хотите видеть продолжения.
Примечание
Ничто не может радовать автора, как оставленный комментарий, даже если это критика 😉
Благодарю за внимание!
ссылка на оригинал статьи http://habrahabr.ru/post/191472/
Добавить комментарий