Populating NSImage with data from an asynchronous NSURLConnection

I have hit the proverbial wall trying to figure out how to populate an NSImage with data returned from an asynchronous NSURLConnection in my desktop app (NOT an iPhone application!!).

Here is the situation.

  • Create NSString by repeating another string a given number of times
  • How do we measure visible bounds of text in Cocoa?
  • Removing leading zeroes from a string
  • Understanding View Controller Nesting in iOS
  • Objective-C retain counts clarification
  • NSString - Convert to pure alphabet only (i.e. remove accents+punctuation)
  • I have a table that is using custom cells. In each custom cell is an NSImage which is being pulled from a web server. In order to populate the image I can do a synchronous request easily:

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

    The problem with this is that the table blocks until the images are populated (obviously because it’s a synchronous request). On a big table this makes scrolling unbearable, but even just populating the images on the first run can be tedious if they are of any significant size.

    So I create an asynchronous request class that will retrieve the data in its own thread as per Apple’s documentation. No problem there. I can see the data being pulled and populated (via my log files).

    The problem I have is once I have the data, I need a callback into my calling class (the custom table view).

    I was under the impression that I could do something like this, but it doesn’t work because (I’m assuming) that what my calling class really needs is a delegate:

    NSImage * myIMage;
    myImage = [myConnectionClass getMyImageMethod];
    

    In my connection class delegate I can see I get the data, I just don’t see how to pass it back to the calling class. My connectionDidFinishLoading method is straight from the Apple docs:

    - (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];
    }
    

    I am hoping this is a simple problem to solve, but I fear I am at the limit of my knowledge on this one and despite some serious Google searches and trying many different recommended approaches I am struggling to come up with a solution.

    Eventually I will have a sophisticated caching mechanism for my app in which the table view checks the local machine for the images before going out and getting them form the server and maybe has a progress indicator until the images are retrieved. Right now even local image population can be sluggish if the image’s are large enough using a synchronous process.

    Any and all help would be very much appreciated.

    Solution Update

    In case anyone else needs a similar solution thanks to Ben’s help here is what I came up with (generically modified for posting of course). Bear in mind that I have also implemented a custom caching of images and have made my image loading class generic enough to be used by various places in my app for calling images.

    In my calling method, which in my case was a custom cell within a table…

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

    This creates an instance of myLoader class and passes it 4 parameters. The URL of the image I want to get, a unique reference that I use to determine which class made the call when setting up the notification observers, the ID of the image, whether I want to save the image to cache or not and the path to the cache.

    My ImageLoaderClass defines the method called above where I set what is passed from the calling cell:

    -(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
        }
    }
    

    In the connectionDidFinishLoading method I saved the file to cache if needed and made a notification call to any listening observers:

    - (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];
    }
    

    Finally in the table controller set up an observer to listen for the notification and send it off to the method to handle re-displaying the custom cell:

    -(id)init
    {
        [super init];
    
        NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
    
        [nc addObserver:self selector:@selector(updateCellData:) name:@"myUniqueRef" object:nil];
    
        return self;
    }
    

    Problem solved!

    3 Solutions Collect From Internet About “Populating NSImage with data from an asynchronous NSURLConnection”

    Your intuition is correct; you want to have a callback from the object which is the NSURLConnection’s delegate to the controller which manages the table view, which would update your data source and then call -setNeedsDisplayInRect: with the rect of the row to which the image corresponds.

    My solution is to use Grand Central Dispatch (GCD) for this purpose, you could save the image to disc too in the line after you got it from the server.

    - (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;
    }
    

    (I am using Automatic Reference Counting (ARC) therefore there are no retain and release.)

    Have you tried using the initWithContentsOfURL: method?