Интересные статьи об Apple, приложениях для iPhone и iPad, iTunes

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

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

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

img



Содержание:

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



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

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

Примерно (в процессе решим что добавить/исключить) таким образом будет выглядеть наше приложение после нескольких часов (а может и минут) упорного программирования:
img

img



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

Поехали…

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


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

img
img



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

img
Добавление и настройка 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



Запускаем:

img


Осталось реализовать методы делегатов 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

img



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

img


В 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"]];


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

img


В 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

<code class="objectivec">#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


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

img
img
Десерт из Core Data

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

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

Для обработки факта отсутствия интернет соединения мы воспользуемся следующим методом из протокола VKRequestDelegate:
- (void)VKRequest:(VKRequest *)request
        connectionErrorOccured:(NSError *)error
{
//    TODO
}


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

Ах да, совсем забыл! Подключаем CoreData.framework.

img

Добавляем три любимые нами свойства в 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:

img



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

img



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

img



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

img


Завершаем и закрываем 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();
        }
    });
}


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

img


Такс, осталось доработать отображение таблицы при обрыве интернет соединения. Весь код пойдёт в метод - (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;
}



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

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


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

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

Примечание


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

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

Автор: AndrewShmig