Заполнение NSImage данными из асинхронного NSURLConnection

Я столкнулся с пресловутой стеной, пытаясь понять, как заполнить NSImage данными, возвращаемыми из асинхронного NSURLConnection в моем настольном приложении (НЕ приложение для iPhone!!).

Вот ситуация.

У меня есть таблица, в которой используются пользовательские ячейки. В каждой пользовательской ячейке находится NSImage, который извлекается с веб-сервера. Чтобы заполнить изображение, я могу легко выполнить синхронный запрос:

myThumbnail = [[NSImage alloc] initWithContentsOfFile:myFilePath];

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

Поэтому я создаю класс асинхронного запроса, который будет извлекать данные в своем собственном потоке в соответствии с документация Apple. Нет проблем. Я вижу, как данные извлекаются и заполняются (через мои файлы журнала).

У меня проблема в том, что когда у меня есть данные, мне нужен обратный вызов в моем вызывающем классе (настраиваемое представление таблицы).

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

NSImage * myIMage;
myImage = [myConnectionClass getMyImageMethod];

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

- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
    // do something with the data
    // receivedData is declared as a method instance elsewhere
    NSLog(@"Succeeded! Received %d bytes of data",[receivedData length]);

    // release the connection, and the data object
    [connection release];
    [receivedData release];
}

Я надеюсь, что это простая проблема для решения, но я боюсь, что мои знания в этом вопросе на пределе, и, несмотря на некоторые серьезные поиски в Google и использование множества различных рекомендуемых подходов, я изо всех сил пытаюсь найти решение.

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

Любая помощь будет очень признательна.

Обновление решения

В случае, если кому-то еще понадобится подобное решение, благодаря помощи Бена, вот что я придумал (конечно, в целом модифицированный для публикации). Имейте в виду, что я также реализовал пользовательское кэширование изображений и сделал свой класс загрузки изображений достаточно универсальным, чтобы его можно было использовать в различных местах моего приложения для вызова изображений.

В моем методе вызова, который в моем случае был пользовательской ячейкой в ​​таблице...

ImageLoaderClass * myLoader = [[[ImageLoaderClass alloc] init] autorelease];

    [myLoader fetchImageWithURL:@"/my/thumbnail/path/with/filename.png"
                     forMethod:@"myUniqueRef"
                        withId:1234
                   saveToCache:YES
                     cachePath:@"/path/to/my/custom/cache"];

Это создает экземпляр класса myLoader и передает ему 4 параметра. URL-адрес изображения, которое я хочу получить, уникальная ссылка, которую я использую для определения того, какой класс сделал вызов при настройке наблюдателей уведомлений, идентификатор изображения, хочу ли я сохранить изображение в кэше или нет, и путь в кеш.

Мой ImageLoaderClass определяет метод, названный выше, где я устанавливаю то, что передается из вызывающей ячейки:

-(void)fetchImageWithURL:(NSString *)imageURL 
           forMethod:(NSString *)methodPassed 
              withId:(int)imageIdPassed 
         saveToCache:(BOOL)shouldISaveThis
           cachePath:(NSString *)cachePathToUse
{

    NSURLRequest *theRequest=[NSURLRequest requestWithURL:[NSURL URLWithString:imageURL]
                                          cachePolicy:NSURLRequestUseProtocolCachePolicy
                                      timeoutInterval:60.0];

    // Create the connection with the request and start loading the data

    NSURLConnection *theConnection=[[NSURLConnection alloc] initWithRequest:theRequest delegate:self];

    if (theConnection) {

        // Create the NSMutableData that will hold
        // the received data 
        // receivedData is declared as a method instance elsewhere

        receivedData = [[NSMutableData data] retain];

        // Now set the variables from the calling class

        [self setCallingMethod:methodPassed];
        [self setImageId:imageIdPassed];
        [self setSaveImage:shouldISaveThis];
        [self setImageCachePath:cachePathToUse];

    } else {
        // Do something to tell the user the image could not be downloaded
    }
}

В методе connectionDidFinishLoading я сохранил файл в кэше, если это необходимо, и сделал вызов уведомления всем прослушивающим наблюдателям:

- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
    NSLog(@"Succeeded! Received %d bytes of data",[receivedData length]);

    // Create an image representation to use if not saving to cache
    // And create a dictionary to send with the notification    

    NSImage * mImage = [[NSImage alloc ] initWithData:receivedData];
    NSMutableDictionary * mDict = [[NSMutableDictionary alloc] init];

    // Add the ID into the dictionary so we can reference it if needed

    [mDict setObject:[NSNumber numberWithInteger:imageId] forKey:@"imageId"];

    if (saveImage)
    {
        // We just need to add the image to the dictionary and return it
        // because we aren't saving it to the custom cache

        // Put the mutable data into NSData so we can write it out

        NSData * dataToSave = [[NSData alloc] initWithData:receivedData];

        if (![dataToSave writeToFile:imageCachePath atomically:NO])
    NSLog(@"An error occured writing out the file");
    }
    else
    {
        // Save the image to the custom cache

        [mDict setObject:mImage forKey:@"image"];
    }

    // Now send the notification with the dictionary

    NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];

    [nc postNotificationName:callingMethod object:self userInfo:mDict];

    // And do some memory management cleanup

    [mImage release];
    [mDict release];
    [connection release];
    [receivedData release];
}

Наконец, в контроллере таблицы настройте наблюдателя для прослушивания уведомления и отправки его методу для обработки повторного отображения пользовательской ячейки:

-(id)init
{
    [super init];

    NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];

    [nc addObserver:self selector:@selector(updateCellData:) name:@"myUniqueRef" object:nil];

    return self;
}

Проблема решена!


person Hooligancat    schedule 16.11.2009    source источник


Ответы (3)


Ваша интуиция верна; вы хотите иметь обратный вызов от объекта, который является делегатом NSURLConnection, к контроллеру, который управляет представлением таблицы, который будет обновлять ваш источник данных, а затем вызывать -setNeedsDisplayInRect: с прямоугольником строки, которой соответствует изображение.

person Ben Stiglitz    schedule 16.11.2009
comment
Бен, спасибо за ответ. Не могли бы вы порекомендовать подход добавления наблюдателя в мой вызывающий класс и отправки уведомления из процесса connectionDidFinishLoading и передачи объекта таким образом? - person Hooligancat; 17.11.2009
comment
Это определенно сработает; просто передайте словарь или другую структуру, которая содержит как изображение, так и ресурс, которому он соответствует. Если вы выберете подход с локальным кешем, который вы упомянули в своем вопросе, вы можете напрямую поговорить с ним. - person Ben Stiglitz; 17.11.2009
comment
Бен, спасибо за помощь. Сработало удовольствие. Я обновил свой текст выше, добавив решение для всех, кто может столкнуться с подобной проблемой. - person Hooligancat; 18.11.2009

Мое решение состоит в том, чтобы использовать для этой цели Grand Central Dispatch (GCD), вы также можете сохранить образ на диск в строке после того, как получите его с сервера.

- (NSView *)tableView:(NSTableView *)_tableView viewForTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row
{
    SomeItem *item = [self.items objectAtIndex:row];
    NSTableCellView *cell = [_tableView makeViewWithIdentifier:tableColumn.identifier owner:self];
    if (item.artworkUrl)
    {
        cell.imageView.image = nil;
        dispatch_async(dispatch_queue_create("getAsynchronIconsGDQueue", NULL), 
        ^{
            NSURL *url = [NSURL URLWithString:item.artworkUrl];
            NSImage *image = [[NSImage alloc] initWithContentsOfURL:url];
            cell.imageView.image = image;        
        });
    }
    else
    {
        cell.imageView.image = nil;    
    }
    return cell;
}

(Я использую автоматический подсчет ссылок (ARC), поэтому нет сохранения и освобождения.)

person Jeena    schedule 08.11.2011

Вы пробовали использовать метод initWithContentsOfURL:?

person Marco Mustapic    schedule 16.11.2009
comment
Марко Я пробовал, но это также блокирующий вызов. - person Hooligancat; 16.11.2009