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

в 17:01, , рубрики: andrewshmig, core data, data store, iOS, mobile development, objective-c, разработка под iOS, метки: , , , ,

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

Примечание

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

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

Автор: AndrewShmig

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js