Friday, February 21, 2014

iOS - Asynchronous image loading

There are some tasks in APP programming that occur in almost every APP and then usually more then once. One of these tasks is image loading, which means not images that are contained in the assets of the APP, but coming from external sources.

We have some APPs in our stores that load images for being displayed in UIWebViews others for UIImageViews. The latter can be a bit tricky when they are contained in a UITableViewCell because then they need to be loaded in a way that doesn't intercept the scrolling of the table. Furthermore if the users is scrolling from the top to the bottom of the table and back u'll probably want to load the images just once and have them cached for their subsequent displays.

In short we are talking here about asynchronous loading and caching.

  • Asynchronous loading is not a big problem, but it usually needs some coding effort, but done once for your cell u'll have the smooth scrolling of your table back
  • Caching in short can be just a simple Dictionary with URLs and images (in reality its a bit more complex, but the general idea is that.
Fortunately we as iOS developers are lucky as this functionality exists as a custom library on github.

The SDWebImage-library does a lot more things that are worth to explore, but I'll focus just on the asynchronous loading possibility of images in this post. Lets make it short and show directly some code


- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    Module *module =self.model.modules[indexPath.row];
    
    static NSString *CellIdentifier = @"ModuleTableCell";
    ModuleTableCell *cell = [self.tableView dequeueReusableCellWithIdentifier:CellIdentifier];
    
    [..]

    if (module.imageUrl) {
        [cell.coverImageIndicatorView startAnimating];
        NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:@"%@%@/rest%@",kCasServiceServer,kCasServicePath,module.imageUrl]];
        [cell.coverImageView setImageWithURL:url placeholderImage:[UIImage imageNamed:@"loadingCover-Module"] options:0
                                    progress:^(NSUInteger receivedSize, long long expectedSize) {
                                        // progression tracking code
                                    }
                                   completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType) {
                                       [cell.coverImageIndicatorView stopAnimating];
                                   }];
    } else {
        [cell.coverImageView setImage:[UIImage imageNamed:@"noCover-Module"]];
    }
    return cell;
}


It's not the complete method body, but the interesting parts. I've even put the class names that I recently used in one of my projects, so don't be confused; "Module" and "ModuleTableCell" are custom classes.

So what I did here... The "setImageWithURL" method comes from the SDWebImage framework. The url is used automatically for caching the image and during the load it will show the placeholder image. Furthermore during the loading you'll have the possibility to show a progress indicator which can be removed on completing.

Actually in most cases you're done now, however most images that I had to display need to provide an authentication token in the header of the request. That's a problem, because the system does not offer this functionality. SDWebImage has another function where u can manually download and cache an image which allows to set headers, but it is not as comfortable as this single method call.

To achieve this I was using a custom fork that allowed me to add custom headers to the request. Fortunately they added this feature in version 3.5 of SDWebImage which made my fork obsolete. Actually u can define in a static place (or wherever u have access to your ticket creation system) a method like this.

    // This code adds the header to a specific request, so the ticket is validated
    SDWebImageManager.sharedManager.imageDownloader.headersFilter  = ^NSDictionary *(NSURL *url, NSDictionary *headers)
    {
        NSMutableDictionary *mutableHeaders = [headers mutableCopy];
        [mutableHeaders removeObjectForKey:@"Authorization"];
        NSString *pt = [ProxyTicketHelper.sharedHelper proxyTicket:url];
        DLog(@"Add Authorization sapped header (%lu) for URL %@",(unsigned long)[mutableHeaders count], [url description]);
        [mutableHeaders setValue:[NSString stringWithFormat:@"PTAUTH %@", pt] forKey:@"Authorization"];
        
        return mutableHeaders;
    };

This block is added just once, but executed on every request just a few instructions before the real request is sent. That's crucial in my case because the tickets are one-time-tickets and need to reach the server in the right order.

The server in my case evaluates the ticket, obtains the user information and sends me the image if I'm allowed to get it. That's it!

No comments: