Tuesday, June 4, 2013

iOS: How To Download Images Asynchronously (And Make Your UITableView Scroll Fast) http://natashatherobot.com/ios-how-to-download-images-asynchronously-make-uitableview-scroll-fast/

http://natashatherobot.com/ios-how-to-download-images-asynchronously-make-uitableview-scroll-fast/

iOS: How To Download Images Asynchronously (And Make Your UITableView Scroll Fast)

Posted on May 25th, 2013

I've recently been working on an app called FoodSquare, it's basically Yelp sorted by Foursquare checkins instead of user reviews. Just like in the Yelp app, I wanted to populate an image for each restaurant on my UITableView. It's not done yet, but here is a screenshot:

To get the images to load, I started out by storing the image URL that was returned via the FourSquare API, and then in the tableView:cellForRowAtIndexPath: method, getting the image data through the image url like this:

1
2
3
4
5
6
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
   Venue *venue = ((Venue * )self.venues[indexPath.row]);
   cell.imageView.image = [UIImage imageWithData:
                                   [NSData dataWithContentsOfURL:venue.imageURL]];
}

While this seems reasonable, when I actually loaded up my table view, I noticed that scrolling was really slow. The UIImage imageWithData: method is NOT asynchronous, so as the table view loads each new cell, it has to go out to the image url and download the data, locking up the app while doing so.

So I made it my mission this weekend to figure out how to get the imagesasynchronously. Luckily, my friend, an iOS developer, Nick O'Neill was in town, and gave me a few amazing pointers on how to load the images according to Apple's recommendation.

First, Nick pointed me to the SDWebImage library, that has a lot of features for working with images, including getting them asynchronously. However, if you're just looking for that one functionality, there is a simple way to handle image downloads by first creating this method:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- (void)downloadImageWithURL:(NSURL *)url completionBlock:(void (^)(BOOL succeeded, UIImage *image))completionBlock
{
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
    [NSURLConnection sendAsynchronousRequest:request
                                       queue:[NSOperationQueue mainQueue]
                           completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) {
                               if ( !error )
                               {
                                    UIImage *image = [[UIImage alloc] initWithData:data];
                                    completionBlock(YES,image);
                                } else{
                                    completionBlock(NO,nil);
                                }
                           }];
}

Basically, you send in a URL (or you can modify the method to send in a url string if you want) and a completion block, and do a normal asynchronous url request to the image url. You know that the data that will be returned will be an image, which you instantiate, and pass in to the passed in completion block.

Now, in my tableView:cellForRowAtIndexPath: method, I can download the image and save it asynchronously:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    static NSString *cellIdentifier = @"venue";
    UITableViewCell *cell = [self.tableView dequeueReusableCellWithIdentifier:cellIdentifier forIndexPath:indexPath];
 
    if (!cell) {
        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:cellIdentifier];
    }
 
    Venue *venue = ((Venue * )self.venues[indexPath.row]);
    if (venue.userImage) {
        cell.imageView.image = venue.image;
    } else {
        // set default user image while image is being downloaded
        cell.imageView.image = [UIImage imageNamed:@"batman.png"];
 
        // download the image asynchronously
        [self downloadImageWithURL:venue.url completionBlock:^(BOOL succeeded, UIImage *image) {
            if (succeeded) {
                // change the image in the cell
                cell.imageView.image = image;
 
                // cache the image for use later (when scrolling up)
                venue.image = image;
            }
        }];
    }
}

I really love the use of blocks in the downloadImageWithURL:completionBlock: – it finally makes sense to me why block are so important in iOS development. Looking forward to using this type of pattern a lot more!

No comments:

Post a Comment