iOS app Design Blog: How to load images asynchronously inside UITableView

We are back, once again!

Which hosting company is trusted by over 9 million websites? Find out here!

One of the most common scenarios that iOS app devs face is when they have to load images from a network resource. While this is just first part of the challenge, you need to decide how you want to do it – synchronously or asynchronously.

iOS app design challenge: load images asynchronously

iOS app design needs to take care of many things. And great UX is first & last among them. Most of the time, you are required to display images inside a table / collection view. And it is implied that there is lot of information to be presented to the user at once. In apps for online shopping, catalogs – this becomes real challenge for iOS programmers because user is quite anxious to see a hundred items in a single table view, in least possible amount of time.

As a result, synchronous loading almost makes things impossible to run. This is because all UI updates occur on main queue, which in turn does its work through main thread. Fetching images synchronously on main thread directly kills user experience.

The risk? Before it turns your customers away, it can get you fired from your iOS Dev’s job!

The answer is async loading, also called lazy loading. In case you are looking for an iOS programmers job, familiarize yourself with this phase – you are going to meet this lazy loading chap somewhere down the line.

So our task for the day is clear: Develop an iOS app design that will:

  • load images from a REST API end point – we covered the initial loading of JSON data in our last tutorial. We will take it as base point and take our journey forward.
  • load them asynchronously i.e. without blocking the UI / displaying appropriate placeholder image
  • display them inside a table view
  • Ensure that scrolling is smooth, there is no overlapping of images, and most importantly: correct images load inside any given UITableviewCell

iOS App Design: steps to load images asynchronously

Our first job is to provide UI for our the JSON data we fetched last time, through our APIDataFetcher component. We will build this tutorial upon that.

If you remember, our APIDataFetcher component is capable enough of notifying its caller to receive the data / receive the error. Remember _successBlock and _failureBlock iVars? APIDataFetcher calls these blocks at proper times. It calls  _successBlock whenever it fetches data, and calls_failureBlock whenever it encounters error.

All we need to do is, supply these blocks, at appropriate points in our UI Code. And those blocks will be invoked, updating our UI with data / error.

Our obvious choice for the UI will be UITableview. We plan to do things in following order:

  1. Create app and import APIDataFetcher.m and APIDataFetcher.h files inside it.
  2. Load data as soon as the view is loaded – through APIDataFetcher – see loadData function below. If data is available, refresh table view using [tableView reloadData] call. If error has occurred, simply show blank, and log error description for now. You can also display some user friendly error message on the UI, which is trivial part for this tutorial.
  3. Derive UITableViewCell to create a subclass. Create XIB for this subclass and attach outlets to it.
  4. Create necessary UITableView datasource and delegate methods to display the data fetched in step 2
  5. While loading data in UITableView, ensure that all images are loaded lazily, that is, asynchronously, and most importantly, correctly. (we will cover why this last part is important aspect). Importantly, this task is not accomplished inside our ViewController but the UITableViewCell derived class that we designed in step 3 above.

Let’s look at each, one by one.

UK Dedicated Servers

Step 1 – Create single view app with table view

This is rather self-explanatory. We need to have at least one view controller that has a table view instance, implements protocols UITableViewDataSource and UITableViewDelegate, and that’s that. Once you are done, be sure to import APIDataFetcher.m and APIDataFetcher.h inside your project, or refer to this link to have entire project at your disposal.

Step 2 – Load data through APIDataFetcher

Take a look at loadData function below, which is called from viewDidLoad. Why call it from viewDidLoad? Simple – to avoid network call. Because it should only do REST API call if the entire view was dumped out of memory and loaded again.

If it was made part of viewDidAppear, it will be called every time even when you come back from a popped or modal view controller, or come back from a phone call.

- (void) loadData
{
    if (!resultArray)
    {
        resultArray = [[NSMutableArray alloc] init];
    }
    else
    {
        [resultArray removeAllObjects];
    }

    [self startActivityIndicator];
    [APIDataFetcher loadDataFromAPI:API_URL :^(id result)

    {
        [self stopActivityIndicator];
        if ([result isKindOfClass:[NSDictionary class]])
        {
            NSArray * resultsArrayfromJSON = (NSMutableArray*)[(NSDictionary*)result valueForKeyPath:@"results"];       

            for (NSDictionary * resultDict in resultsArrayfromJSON)
            {
                Track * track = [[Track alloc] initWithDictionary:resultDict];
                [resultArray addObject:track];
            }            
            [_tableView reloadData];
        }

    } :^(NSError *error)

    {
        [self stopActivityIndicator];
        if (error)
        {
            NSLog(@"%@", error.localizedDescription);
        }

    }];

}

What’s going on? It’s simple as 1-2-3. We are using the two blocks that we passed to APIDataFetcher as arguments in our previous tutorial. But their importance is being known now.

Blocks are powerful – and you don’t realize it when you invoke them. You realize it when you define when will you invoke them.

Another small thing to note is that we are using resultArray – which is to cache the APIDataFetcher response inside our view controller. Note that in real, large scale apps, this iVar can reside anywhere other than a view controller, in order to make it sharable across various view controllers. Here, for simplicity’s sake, we have kept it inside ViewController.

startActivityIndicator and stopActivityIndicator calls are nothing but ways to manipulate UIActivityIndicatorView’s visibility – that piece of code is self-explanatory.

So, what’s the big deal? You call it ‘the iOS app design blog’:

The heart of loadData is a major design decision we took, in case you haven’t seen it so far: The Track Object. This object stores whatever data REST API supplies it.

But why on earth, the Track? Oh yes, I forgot to tell you – Track is a model object, so its name shows what it stores. The track information. Come on! what tracks? Well, well, this deserves an explanation, but is a no-brainer. For our example’s sake, we are fetching iTunes live track data, the URL being: https://itunes.apple.com/search?term=Tom. This provides JSON response that is list of Track Details, and here is one – out of the list:




 

 

{
 "resultCount":50,
 "results": 
[
{
  "artistName": "Brad Bird", 
  "artworkUrl100": "http://is1.mzstatic.com/image/thumb/Video6/v4/c5/ab/8e/c5ab8e28-11b8-11a0-77db-cf0b77da27ce/source/100x100bb.jpg", 
  "artworkUrl30": "http://is1.mzstatic.com/image/thumb/Video6/v4/c5/ab/8e/c5ab8e28-11b8-11a0-77db-cf0b77da27ce/source/30x30bb.jpg", 
  "artworkUrl60": "http://is1.mzstatic.com/image/thumb/Video6/v4/c5/ab/8e/c5ab8e28-11b8-11a0-77db-cf0b77da27ce/source/60x60bb.jpg",  
  "longDescription": "From Disney comes two-time Oscar\u00ae winner Brad Bird\u2019s \u201cTomorrowland,\u201d a riveting mystery adventure starring Academy Award\u00ae winner George Clooney. Bound by a shared destiny, former boy-genius Frank (Clooney), jaded by disillusionment, and Casey (Britt Robertson), a bright, optimistic teen bursting with scientific curiosity, embark on a danger-filled mission to unearth the secrets of an enigmatic place somewhere in time and space known only as \u201cTomorrowland.\u201d What they must do there changes the world\u2014and them\u2014forever.", 
  ...,
  ...
},
 ...,
 ...,
 ]
}

If you are familiar with JSON serialization, you will know that every JSON element can be stored into either an array / dictionary. Dictionaries have key/value pairs, which you can see above, such as artistName, country etc. Our APIDataFetcher gives us the JSON serialized data, and we extract results array – the top level element. And we then extract dictionaries from this array.

We have our data with us, in the form of dictionaries. We can right away display it. Why the hell Track object still trolling around? We need it, because we need to wrap this dictionary information into something meaningful, called Track. Dictionaries can store Artists too, but with Track, we know what kind of dictionary we are dealing with. And that’s the first principle of Object Oriented Programming: Everything is Object.

There are obvious and immediate advantages of this approach – the first one being, you can have properties inside Track Object that map to above dictionary keys. Another advantage is extensibility. If you try to add one more piece of track info inside your app, you don’t have to perform nasty search for dictionaries through out your code to see where your Dictionaries mean Tracks. As soon as you get dictionary from JSON, you convert it into a Track and do whatever you want with it, until you need to send it across the network, at which point you need to serialize it again to a dictionary.

OK, OK,  so Track is what it is. How does it wrap the dictionary data? It possesses just one method that does it:

//  Track.m
- (instancetype) initWithDictionary : (NSDictionary *) dictionary
{
    self = [super init];
    
    if (self)
    {
        _trackArtist = [dictionary valueForKey:@"artistName"];
        _trackDescription = [dictionary valueForKey:@"shortDescription"];
        _trackImgURL = [dictionary valueForKey:@"artworkUrl60"];
    }
    
    return self;
}

Track has following properties, and as you need more, you just need to add more mappings to above init method, and you are done. Every time, create a Track object, and type “track." – and XCode will do the rest for you.

Step 3 – Derive UITableViewCell to create a subclass

This one is rather easy. Just look around for examples how to add a subclass to existing iOS class (UITableViewCell), and associate an XIB with it, with some outlets mapped to the subclass .h file. Here is the code for .h file.

#import <UIKit/UIKit.h>
#import "Track.h"

@interface APITableViewCell : UITableViewCell
@property (weak, nonatomic) IBOutlet UILabel *labelDescription;
@property (weak, nonatomic) IBOutlet UILabel *labelArtist;
@property (weak, nonatomic) IBOutlet UIImageView *cellImageView;
- (void) bindDataWithCell : (Track *) track :(NSIndexPath *) indexPath :(UITableView*) tableView;
@end

The property names are self explanatory, and they will, at some point, hold Track artist, Track description, and Track image (fetched from one of the URLs specified in REST API JSON response, shown at the top). Let’s not discuss the source file for APITableViewCell, because that is where the asynchronous image loading logic takes place.

Step 4 – Create necessary UITableView datasource and delegate

In this step, we shall cover the bare bones of UITableView delegate and datasource. Who is that? No one else – but our view controller itself. In the grand design, our view controller often gets to play the role of delegate and data source every time collection of items are to be dealt with. Data source methods tells where the item data is coming from, and how many of them are lined up. Delegate methods controls the UI related behavior – selection of cells, appearance of cells etc. Without going into details, here they are:

- (void) registerCells
{
    [_tableView registerNib:[UINib nibWithNibName:@"APITableViewCell" bundle:[NSBundle mainBundle]] forCellReuseIdentifier:@"APITableViewCell"];
}

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
    return 1;
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    NSInteger count = [resultArray count];

    return count;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    APITableViewCell * cell = [_tableView dequeueReusableCellWithIdentifier:@"APITableViewCell"];

    if (!cell)
    {
        cell = [[APITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"APITableViewCell"];
    }
    
    Track * track = [resultArray objectAtIndex:indexPath.row];
    
    if (track)
    {
        [cell bindDataWithCell:track :indexPath :tableView];
    }
    
    return cell;
}

The first method registerCells is not a datasource method, the rest three are. Inside registerCells, registerNib is to tell the table view that since we are using the same layout for each cell, queue it. We also tell the XIB name to fetch the layout from. In a scrollable view with thousand cells, it doesn’t instantiate UITableViewCell (read view) each time you scroll. Instead, it just creates a screenful of them. Once you scroll down the screen, the ones that go up are queued, and the ones that come down are displayed – or dequeued from the list of those which are queued – off course, filtered by the XIB name which was used to register the cell. And this dequeueing happens inside the last method – cellForRowAtIndexPath. Instead of doing [alloc] init with a new APITableViewCell each time, it gets one from the table view method: dequeueReusableCellWithIdentifier. This wouldn’t be possible without registerNib – that’s the use of registerCells.

The next two, numberOfSectionsInTableView and numberOfRowsInSection simply tells table view to load 1 section, and load all tracks that are fetched from REST API JSON response into resultsArray.

The last one, cellForRowAtIndexPath is where the magic must happen. Where else  the asynchronous loading of images in table view will succeed?

Here, dequeueReusableCellWithIdentifier tries to fetch some preloaded cell, and if it fails, we alloc init it. Then, we go on loading the Track from resultsArray – the track for this cell.

Surprisingly, without doing much, we return the cell – after doing bindDataWithCell, which is the method of the cell itself. And that is where the asynchronous image loading magic happens.

Remember why we skipped to cover APITableViewCell.m. Well, now we will fully visit it, and see the pieces falling together.

Step 5 – Lazy Loading A.K.A. Asynchronous image loading in UITableView

Lazy Loading obviously means you don’t tie the main UI thread with loading of image data. But it also has a deeper meaning.

Windows Reseller Hosting

Loading something lazily means sometimes you fetch it from local cache (although this blog does not address it). Only if you detect that there is no local data, and / or your business logic requires you to refresh the image, you load it from server, necessitating a REST API call.

One more aspect that’s covered under the term lazy loading is that all you get inside JSON response is not an actual image, but a URL to it. There are obvious advantages of this approach.

  1. Every API call should not need to be burdened with fat images. Why display images if they aren’t needed at all?
  2. Depending on the target device, you may want to provide different dimension images, in which case supplying single image data to form a bulky response is nonsense. Providing different image URL puts the freedom in app client’s hands – you choose your image size and fetch it wherever you want. That’s all about lazy loading.

Here is the implementation of APITableViewCell.m – ignore the setSelected method. Let’s jump inside bindDataWithCell:

- (void)setSelected:(BOOL)selected animated:(BOOL)animated
{
    [super setSelected:selected animated:animated];
}

- (void) bindDataWithCell : (Track *) track :(NSIndexPath *) indexPath :(UITableView*) tableView
{
    static UIImage * placeHolder;

    placeHolder = [UIImage imageNamed:@"placeholder"];
    
    self.labelArtist.text = track.trackArtist;
    self.labelDescription.text = track.trackDescription;
    self.cellImageView.image = placeHolder;
    
    NSString *imageUrl = track.trackImgURL;
    
    BOOL bNeedsToFetch = [self.cellImageView.image isEqual:placeHolder];
    
    if (bNeedsToFetch && imageUrl && ![imageUrl isEqualToString:@""])
    {
        //[NSURLConnection sendAsynchronousRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:imageUrl]] queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *error)
        NSURLSessionTask *task = [[NSURLSession sharedSession] dataTaskWithURL:[NSURL URLWithString: imageUrl] completionHandler:^(NSData * data, NSURLResponse * response, NSError * error)
        {
            if (!error && data)
            {
                UIImage *image = [UIImage imageWithData:data];
                //request succeeded, overwrite cellImage
                if (image)
                {
                    dispatch_async(dispatch_get_main_queue(),
                    ^{
                        APITableViewCell *cell = (id)[tableView cellForRowAtIndexPath:indexPath];
                        if (cell)
                            cell.cellImageView.image = image;
                    });
                }
            }
            else
            {
                NSLog(@"Failed to load the image from URL: %@", imageUrl);
            }
        }];
        [task resume];
    }
}

Inside bindDataWithCell, we get a Track object. This is obvious because it is our Model object. This method will simply read different properties of Track object that’s supplied to it, and sets the cell UI elements – labelArtist and labelDescription text. cellImageView is the imageview where we need to load the images, lazily.

Need better web hosting? Choose 1&1. Free: domains, marketing tools, search engine ad & more. Check OFFERS!

We begin by setting the image to placeholder image, which should be something of a meaning to say ‘wait, I am loading’. Note that it’s a static UIImage variable, which means every call to bindDataWithCell will not set it. It is set just the first time, and reused forever.

Fetching of actual image happens via following statement:

NSURLSessionTask *task = [[NSURLSession sharedSession] dataTaskWithURL:[NSURL URLWithString: imageUrl] completionHandler:^(NSData * data, NSURLResponse * response, NSError * error)

followed by:

[task resume];

Now, the usage of NSURLSessionTask is new, but not very new, since it’s available since iOS 7. It simply fetches the data on a background thread, and when done, it leaves you inside a completionHandler – saying – play with your data, or handle where you screwed up (error). It also hands you an NSURLResponse object, in case you need some additional information.

Irrespective of what you got (valid data or error) – you must remember that you are on background queue! You must fetch everything that you can, while on background queue.

Then, and only then, you should resurrect yourself into the main queue with the image data!

(Well, if you don’t, your image won’t show up probably. Phsst.)

Back inside main queue, however, there is a bigger thing to understand – we get the cell reference, again. Yes, again. Then we set the image view image property = the one we got from NSURLSessionTask call. Focus on the following statements:

APITableViewCell *cell = (id)[tableView cellForRowAtIndexPath:indexPath];
if (cell)
   cell.cellImageView.image = image;

If we are already inside the cell, and this cell is the one that’s being created, why do we need explicit cell reference again? That’s the heart of how dequeueReusableCellWithIdentifier works. Remember that cells are reused. Only visible cells are created by table view. During scrolling, cell instances are queued and dequeued, altering their data only. That’s what cellForRowAtIndexPath we wrote above does – setting the cell data of an already existing OR newly created UITableViewCell (or it’s derivative, APITableViewCell).

When we are inside APITableViewCell instance method, we only know we are dealing with a cell; we don’t know which cell out of all cells that user is able to scroll through.

But we already on the call stack of cellForRowAtIndexPath, that was the method that called bindDataWithCell in the first place. Aren’t we creating recursion?

The answer to that question is, my friend, No. [tableView cellForRowAtIndexPath:indexPath] is a call to cellForRowAtIndexPath belonging to UITableView, which is different from the one with the same name of UITableViewDataSource, implemented by our own view controller which is the data source in present case.

The former one simply returns a cell of the table view, and it must be called explicitly, to get the cell at desired index path. The later method, the one we usually encounter and write, is called by table view during reloadData call, and it is the version implemented by the data source is invoked. It should never be invoked directly.

The case is put to rest. Here is the entire project, you may use it, contribute to it, and share with friends.

Scroll farther as you want. Just don’t screw up with the cells anymore.

Tagged with:

Leave a Reply

Your email address will not be published. Required fields are marked *

*