Joey J
Joey J

Reputation: 1365

Returning a value retrieved in a separate thread using GCD

I've written a custom view controller class that displays a map with annotations. When an annotation is pressed a callout is displayed and a thumbnail image is shown in the left part of the annotation callout. This class asks a delegate to provide the image that is displayed.

- (void)mapView:(MKMapView *)mapView didSelectAnnotationView:(MKAnnotationView *)view
{
    [(UIImageView *)view.leftCalloutAccessoryView setImage:[self.delegate mapViewController:self imageForAnnotation:view.annotation]];
}

The delegate class retrieves the image from the network. To protect the UI from being unresponsive a new thread is created to download the image using GCD.

- (UIImage *)mapViewController:(MapViewController *)sender imageForAnnotation:(id<MKAnnotation>)annotation
{
    NSURL *someURL = [[NSURL alloc] initWithString:@"a URL to data on a network"];
    __block UIImage *image = [[UIImage alloc] init];

    dispatch_queue_t downloader = dispatch_queue_create("image downloader", NULL);
    dispatch_async(downloader, ^{
        NSData *imageData = [NSData dataWithContentsOfURL:someURL];  // This call can block the main UI!
        image = [UIImage imageWithData:imageData];
    });

    return image;
}

Why does the image never display? I was assuming that since the delegate method returns a pointer to an image that at a time in the future is set to valid image data that the thumbnail would eventually update itself. This apparently is not the case...

Upvotes: 1

Views: 1427

Answers (2)

Aaron Hayman
Aaron Hayman

Reputation: 8502

You're creating a block that's executing asynchronously. This means your code created the block to execute, then immediately returned 'image', which was pointing to the new image you created when you initialized the variable: __block UIImage *image = [[UIImage alloc] init]; Remember that when you return an object from a method, you're actually just returning a pointer to that object.

Sometime after the pointer to this new image was returned. The block runs and assigns the pointer to the image it retrieved to the local variable 'image', which is now out of scope of the method (though the block still has it). So now the block has this reference to the image it got but that reference will go away when the block finishes.

One way to fix this would be to run the block synchronously, but that would be defeating the point of dispatching the image retrieval process. What you need to do is provide a block to the function it can call once the image is retrieved, namely assigning the image where it needs to be. This would look something like this:

- (void)mapViewController:(MapViewController *)sender imageForAnnotation:(id<MKAnnotation>)annotation withImageBlock:(void (^)(UIImage *))block{
{
    NSURL *someURL = [[NSURL alloc] initWithString:@"a URL to data on a network"];
    __block UIImage *image = [[UIImage alloc] init];

    dispatch__object_t currentContext = dispatch_get_current_queue();
    dispatch_queue_t downloader = dispatch_queue_create("image downloader", NULL);
    dispatch_async(downloader, ^{
        NSData *imageData = [NSData dataWithContentsOfURL:someURL];  // This call can block the main UI!
        image = [UIImage imageWithData:imageData];
        dispatch_async(currentContext, ^{
            block(image);
        });
    });
}

Please note that I grab the current queue context so that I can execute the block given to me on the same thread it was given to me on. This is really important since the block passed to me could contain UIKit methods, which can only be performed on the main thread.

Upvotes: 3

Catfish_Man
Catfish_Man

Reputation: 41801

Unfortunately, what you're trying to do here is not easy at all. You'd need to create a "future" (http://en.wikipedia.org/wiki/Futures_and_promises) and return that, which ObjC/Cocoa do not have a built in implementation of.

The best you can likely do here is either a) have the caller pass in a block that runs on completion of the download and updates the UI, or b) return a placeholder image and schedule a block to replace the image after it finishes downloading. Both of those will require restructuring your code somewhat. The latter also requires your downloading code to know how to update your UI, which is a bit unfortunate in terms of increasing coupling.

Upvotes: 1

Related Questions