Читатели, добрый день!
Сегодня хочу начать вольный перевод книги Михаеля Привата и Роберта Варнера «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.
Надеюсь, что Вы хотите видеть продолжения.
Примечание
Ничто не может радовать автора, как оставленный комментарий, даже если это критика ;)
Благодарю за внимание!
Автор: AndrewShmig