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

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



    Содержание:
    • Глава №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.
    Надеюсь, что Вы хотите видеть продолжения.

    Примечание

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

    Благодарю за внимание!
    Поделиться публикацией

    Похожие публикации

    Комментарии 24
      +5
      Непривычно видеть AppCode в статье про iOS.
      Продолжайте
        0
        Честно сказать, только недавно постиг Core Data и использую в основном с SQLite базами данных.

        Предположим, мне нужно будет хранить много картинок и много текста (а вдруг и видео?) — разумно ли будет все еще использовать SQLite или стоит задуматься о других типах хранения данных через CoreData? А какие есть?

        А еще в некоторых туториалах видел автоматическое создание классов к каждой сущности в модели, но так и не нашел кнопочку в Xcode. А использовать valueForKey: не особо приятно (фу на эти магические стринги). Можете подсказать, куда копать ради этих самых авто классов?
          0
          Вам пригодится.
          Видео, картинки, аудио я бы хранил на диске.

          В «Практической части» я использую valueForKey: только потому, что этот метод используется в «Теоретической части». Не хотелось вводить людей в заблуждение. Всему своё время.
            0
            Спасибо!
            Точно! :) Ведь в базе можно хранить просто ссылки на файлы
              0
              а еще есть опция «Store in External Record File». Тогда блобы более 100 кб Core Data сама будет выносить в файлы. И не надо будет подчищать их за собой.
            –1
            мне казалось, что в 200х уже перевелись те, кто хранит все в БД :)
            0
            и как Вам AppCode? Есть свои плюсы минусы?
              0
              Для меня больше плюсов, чем минусов.
              Нравится, а быть точнее — влюблен :)
                +2
                Плюсы — намного более адекватные рефакторинги и вообще работа с кодом. Вставлять руками импорты в Xcode в 2013 году это meh.
                +1
                Пост совсем о другом, но тем не менее, мне хотелось бы обсудить GDC и его использование в примере из статьи. Правильны ли мои рассуждения:

                1) подобная конструкция не является безопасной, так как используется один и тот же managedObjectContext в разных потоках.
                /    сохраняем данные в фоне, чтобы не замораживать интерфейс
                    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();
                        }
                

                И по-хорошему следует создать отдельный NSManagedContext для бэкграунда? Или я все же ошибаюсь?

                2) Этот код загрузки изображения в ячейку таблицы может работать некорректно.
                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];
                        });
                    });
                

                Это связано с переиспользованием cell'ов в UITableView. Рассмотрим ситуацию, когда во время загрузки аватарки для 4 ячейки пользователь прокрутил таблицу вниз. Эта 4 ячейка (в блоке она фигурирует как cell) уже станет 10-15, из-за
                [tableView dequeueReusableCellWithIdentifier:cellID]; и у нас получится так, что аватарка для 4 ячейки вдруг отрисовался у 10-15 ячейки.

                  +1
                  1) Запись всегда происходит из одного потока, поэтому в данном примере это не так страшно.
                  2) Рассмотрим ситуацию: пользователь запускает приложение, перед ним таблица и аватарки загружаются. Во время загрузки пользователь крутит таблицу вверх и начинается загрузка аватарок для соответствующих ячеек. Так вот в то время, когда dequeueReusableCellWithIdentifier:cellID: вернет ячейку, которая была ранее использована, изображение в ней будет сперва «сброшено» на изображение по умолчанию (картинка с буквой S), а уже после загрузки изображения в фоне оно будет установлено в ячейке. Поэтому работает всё корректно.
                  Или я Вас не правильно понял?
                    +1
                    В идеале, нужно расширить UIImageView, и хранить в нём ссылку, которая предполагается к загрузке.
                    Перед установкой изображения проверять, эквивалентны ли ссылки
                      +1
                      1) Опасна не только одновременная запись из нескольких потоков. Что если из UI-потока во время этого сохранения сработает fault, например? Получим одновременное обращение к контексту из разных потоков.
                      2) Все-таки проблема есть. Если картинка успела загрузиться в ячейку, все произойдет как Вы написали, картинка просто сбросится на дефолтную и начнет грузиться новая. А что если cell.imageVIew.image =… сработает только когда ячейка УЖЕ используется для другого объекта? Тут есть два варианта:

                      • Запоминать не ячейку, а index path, и устанавливать картинку, получая текущую ячейку по запомненному index path. Однако этот метод усложняется, если возможны вставки/удаления строк в таблице
                      • Хранить в ячейке objectID объекта, который там сейчас отображается. Далее все просто: когда картинка загружена, просматриваем visible cells и ищем там ячейку с нужным objectID
                      • И вот выше подсказывают, хранить в ячейке URL желаемой картинки
                        +1
                        2) Действительно, прошляпил такой вариант развития событий. Учтем и, спасибо за замечания (Dreddik).
                          +1
                          1) Если работать через gdc, то код из очереди будет выполняться последовательно в одном потоке (по умолчанию)
                            +1
                            Стоп, только если 1. Вы гарантируете использование этого контекста только внутри очереди 2. Очередь serial, а не concurrent.

                            Но, учитывая оба условия, не проще использовать queue concurrency type и performBlock: или performBlockAndWait:?
                              +1
                              GCD — Grand Central Dispatch, а не GDC :)
                                0
                                Очепятка же...)
                        +3
                        Автор, большое спасибо за перевод. Пожалуйста, продолжай в том же духе! Хорошую работу проделал.
                          +1
                          Вам спасибо за отзыв! Именно такие отзывы придают сил для последующих переводов!
                          • НЛО прилетело и опубликовало эту надпись здесь
                          0
                          Спасибо большое за статьи. Не забрасывайте пожалуйста работу. Отдельное спасибо за хороший перевод.

                          P.S. Пробывал повторить приложение ваше шаг за шагом. Заткнулся на подключении библиотеки по работе с vk. У меня в проекте появились варнинги и проект падает при запуске, думаю что это из-за того что у меня sdk 7.0.
                          0
                          Благодарю за отзыв!

                          Насчет PS ничего толком не могу сказать, с тех пор версия Вконтакте SDK сильно видоизменилась и фиксов много было (см GitHub). Если всё таки решите добить, то пишите с вопросами в личку, постараюсь помочь.

                          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                          Самое читаемое