Smart Data Loading in Table Views (Automatic UITableView Paging)

You can take any user interface in the world, and whether it’s gorgeous and intuitive or ugly and clunky, there’s one unifying factor that will right pretty much any wrong: speed. Think about it. The load screen, the spinning beachball, the three-seconds of stutter before your click registers as a click–those are the worst experiences you’ve ever had with any user interface. And they’re all related to core responsiveness.

1. Foreword

Today I found this great article called “The 3 White Lies Behind Instagram’s Lightning Speed”, explaining the underlying secrets of why Instagram simply feels faster than other apps.

For instance, when you’re captioning a photo for upload, Instagram will already be pulling potential stores and restaurants that match your GPS location for checking in, and they’ll have been uploading the photo itself (though not sharing it) since you selected a filter. This cuts down, or even eliminates, all possible lag time on the next few screens that the user visits. Just because you need a few moments to think of a funny caption doesn’t mean that your cell phone can’t tackle a few other jobs while it waits.

Here are two pictures enclosed in the article that pretty much explained all about it.


2. Smart data loading

Speed and responsiveness, especially in a mobile platform, pretty much define the users’ overall feeling. In an world built for faster and faster pace, nobody likes to wait. Building fast apps is a goal we all have to strive for.

After reading the article, I soon decided to modify part of my app I was working with. Part of the app includes a search form, where users can type in keywords and get search results from QuizLet. However, a dilemma is presented. One can either load more search queries per batch (which increases the startup loading speed), or load less search results (which increases the need for constant reloading). In either case, the user won’t be happy.

It is due to this reason that I came up with a relatively smarter data loading system.

  1. Load relatively less amount of data on startup. This increases startup loading speed, and populates the table with data faster, so the user won’t have to wait for too long before any data shows up.
  2. Whenever the user scrolls to two-thirds of the current result list, the app automatically starts fetching for the next batch of results.Therefore at most times, the user won’t have to wait for the data to reload, because whenever he scrolls to the near bottom, the table automatically is refilled with new data.
  3. Although the system pre-loads data, it is still possible that the user scrolls through the table at a very fast speed. The data reloading speed might still not be able to catch up the user. In this case, when the user actually scrolls to the bottom and the data is not catching up, a line running “Loading more…” gets displayed at the bottom. (So the user knows something is still going on).
  4. If the available data is not even enough to fill up a page, a line running “(No More Results Available)” gets displayed at the bottom. (It will let the user know so that the user won’t wait in vain.)

3. Implementation

As can be imagined, the implementation is relatively easy. Here, I’ll post a quick demo of the implementation. Note that
for demo purposes, instead of using real data fetching API, I’m using a three-second manual wait, simulating the time required for data fetching. You can follow along the tutorial below, or you can skip it and simply download the entire demo project here.

Before getting started, I declared these variables in the header file:

// The data source to be displayed in the table (you should store the fetched data in this array)
@property (strong, nonatomic) NSMutableArray *searchResultOfSets;

// The counter of fetch batch.
@property (nonatomic) int fetchBatch;

// Indicates whether the data is already loading.
// Don't load the next batch of data until this batch is finished.
// You MUST set loading = NO when the fetch of a batch of data is completed.
@property (nonatomic) BOOL loading;

// noMoreResultsAvail indicates if there are no more search results.
// Implement noMoreResultsAvail in your app.
// For demo purpsoses here, noMoreResultsAvail = NO.
@property (nonatomic) BOOL noMoreResultsAvail;

Remember to synthesize in the main file:

@synthesize searchResultOfSets;
@synthesize fetchBatch;
@synthesize loading;
@synthesize noMoreResultsAvail;

Next, as usual, drag a UITableView and a prototype UITableViewCell into the storyboard ViewController. Set the Style as Basic, and the Identifier as Cell.

Storyboard setup UITableView and UITableViewCell

Storyboard setup

Moreover, if you did not start off with a master-detail app, you should complete the following steps additionally:

  1. Generate an IBOutlet of UITableView by control-dragging the table view into the code, and name it tableView as the outlet.
  2. Connect the data source and the delegate of the table view to the ViewController.
  3. Change @synthesize tableView in main file to
    @synthesize tableView = _tableView;
    

Drag the circle of delegate and data source to the View Controller icon to connect the data source and delegate.

Next implement or override the following methods. (Note that loadRequest is the method that loads data, which needs your implementation. Moreover, you must set loading=NO whenever the data fetch is completed.)


- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
    // Return the number of sections.
    return 1;
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    // Return the number of rows in the section.
    // If data source is yet empty, then return 0 cell.
    // If data source is not empty, then return one more cell space.
    // (for displaying the "Loading More..." text)
    if (searchResultOfSets.count == 0) {
        return 0;
    } else {
        return ([searchResultOfSets count]+1);
    }
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    static NSString *CellIdentifier = @"Cell";
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
    cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle
                                  reuseIdentifier:CellIdentifier];
    // If scrolled beyond two thirds of the table, load next batch of data.
    if (indexPath.row >= (searchResultOfSets.count /3 *2)) {
        if (!loading) {
            loading = YES;
            // loadRequest is the method that loads the next batch of data.
            // This needs your implementation to load the data into searchResultOfSets
            [self loadRequest];
        }
    }
    // Only starts populating the table if data source is not empty.
    if (searchResultOfSets.count != 0) {
        // If the currently requested cell is not the last one, display normal data.
        // Else dispay @"Loading More..." or @"(No More Results Available)"
        if (indexPath.row < searchResultOfSets.count) {
            cell.textLabel.text = [searchResultOfSets objectAtIndex:indexPath.row];
            return cell;
        } else {
            // The currently requested cell is the last cell.
            if (!noMoreResultsAvail) {
                // If there are results available, display @"Loading More..." in the last cell
                UITableViewCell *cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault
                                                               reuseIdentifier:CellIdentifier];
                cell.textLabel.text = @"Loading More...";
                cell.textLabel.font = [UIFont systemFontOfSize:18];
                cell.textLabel.textColor = [UIColor colorWithRed:0.65f
                                                           green:0.65f
                                                            blue:0.65f
                                                           alpha:1.00f];
                cell.textLabel.textAlignment = UITextAlignmentCenter;
                return cell;
            } else {
                // If there are no results available, display @"(No More Results Available)" in the last cell
                UITableViewCell *cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault
                                                               reuseIdentifier:CellIdentifier];
                cell.textLabel.font = [UIFont systemFontOfSize:16];
                cell.textLabel.text = @"(No More Results Available)";
                cell.textLabel.textColor = [UIColor colorWithRed:0.65f
                                                           green:0.65f
                                                            blue:0.65f
                                                           alpha:1.00f];
                cell.textLabel.textAlignment = UITextAlignmentCenter;
                return cell;
            }
        }
    } else {
        [self.tableView reloadData];
    }
}

As said, for demo purposes, I implemented a sample data loading process.

Create a class named DataLoader. Xcode will generate for you DataLoader.h and DataLoader.m.

Paste the following code in DataLoader.h “as-is”:

#import <Foundation/Foundation.h>
#import "ViewController.h"

@interface DataLoader : NSObject

@property (strong, nonatomic) ViewController *delegate;
- (void)loadData;

@end

Paste the following code in DataLoader.m “as-is”:

#import "DataLoader.h"

@implementation DataLoader

@synthesize delegate;

- (void)loadData
{
    [self performSelector:@selector(loadDataDelayed) withObject:nil afterDelay:3];
}

- (void)loadDataDelayed
{
    delegate.fetchBatch ++;
    NSMutableArray *array = [NSMutableArray arrayWithCapacity:30];
    for (int i = 1; i<=30 ; i++) {
        [array addObject:[NSString stringWithFormat:@"Batch No.:%d, Item No.:%d",delegate.fetchBatch,i]];
    }
    [delegate.searchResultOfSets addObjectsFromArray:array];
    [delegate.tableView reloadData];
    // Always remember to set loading to NO whenever you finish loading the data.
    delegate.loading = NO;
}

@end

Finally, import DataLoader.h in ViewController.m and override the following two methods:

#import "DataLoader.h"

 

- (void)loadRequest
{
    DataLoader *loader = [[DataLoader alloc] init];
    loader.delegate = self;
    [loader loadData];
}

- (void)viewDidLoad
{
    [super viewDidLoad];
    fetchBatch = 0;
    noMoreResultsAvail = NO;
    searchResultOfSets = [[NSMutableArray alloc] init];
    for (int i = 1; i<=30 ; i++) {
        [self.searchResultOfSets addObject:@"The initial batch loaded on startup"];
    }
    [self loadRequest];
    // Do any additional setup after loading the view from its nib.
}

You can now run the project. If nothing went wrong, the project should be running correctly. If you encountered problems, you can download the entire project archive here if you wish.

4. Conclusion

The above is a brief implementation of smart data loading. Building fast and responsive apps is a goal that developers all have to strive for. Technological and networking advancements bring us to an era when it should be the computers (or mobile devices) that are waiting for the users, instead of the vice versa.

Feel free to contact me with any questions or advice. Thank you.



If you enjoyed the article, please share with your friends:

    • Lucas Godoi
    • April 30th, 2013 2:41am

    First, thank you for posting this , is really useful for everyone.
    Second, i`m having a bad time here. I`m trying to load data from web using this code. Every time that i run my code, all data is displayed and the infinite scroll don`t work. What i have to do to block my tableview to load all data and what i have to adapt from your code to mine purpose?
    Anyway,a good-bye from a brazilian programmer!!!

    • Lucas Godoi
    • April 30th, 2013 2:39am

    First, thank you for posting this , is really useful for everyone.
    Second, i`m having a bad time here. I`m trying to load data from web using this code. Every time that i run my code, all data is displayed and the infinite scroll don`t work. What i have to do to block my tableview to load all data and what i have to adapt from your code to mine purpose?
    Anyway,a good-bye from a brazilian programmer .

    • Ramiro Coll Doñetz
    • April 8th, 2013 6:17am

    Hi Ted! How are you? In first place, great post! :)

    My name is Ramiro, I’m an Android/iOS developer from Argentina, I found this post searching another thing. I was searching how to implement the Quizlet API in iOS and I saw that you was be able to do that!

    I’m trying to do that for more than a week and I’m not getting results :(

    I really appreciate if you can throw me a hand with that, maybe I’m missing something, but I’m really blocked at this point.

    Thanks in advance!

    Best regards,
    Ramiro Coll Doñetz.

    P.D.: Sorry for my beginner’s English, I’m learning :)

    • Xandr
    • March 13th, 2013 8:56am

    Thanks! It was very useful!

    a couple of thoughts…
    1. Cells may just dequeued without allocated.
    2. indexPath.row = 2/3 of all searchResultOfSets.count means that an increase in quantities of data, downloads will happen more often.

      • Ted
      • March 14th, 2013 6:30pm

      You are totally right. Maybe the 2nd issue won’t be a big problem. I’ll fix the 1st one.

    • Kersy
    • February 20th, 2013 6:23pm

    This is just the bomb!

    can you P.M me on my email…kersyduru@gmail.com…on how to implement this in a drop down…

    it will be highly appreciated…

    • Michael
    • January 30th, 2013 11:23am

    You have got to be kidding me!! What a beautiful post for “lazy loading”. Very well done. I’ve been looking for a straight-forward, well explained post like this for a few days. Appreciate it.

    • Taylor
    • November 19th, 2012 9:47am

    Thanks for the great article. One question, if you were to pull data from the web to load the table view, what would the strategy be to parse 30 items at a time be? To be a little more clear, how would you stop the parsing at every 30th node, and then start it back up in the same spot when the user scrolls? Cheers!

      • Ted
      • November 20th, 2012 12:18pm

      Thanks for stopping by,

      When I was writing this article, I was using the Quizlet API. The API itself allows me to choose the amount of items fetched, which means I can set the number of items to be fetched in a single network request.

      If you want to employ similar mechanism, you might have to search amongst the API documentation you are trying to implement, and find whether they themselves support fetching a limited number of items in a request.

      You cannot control the number of items yourself, because if you are requesting a JSON/ XML file, it has to be complete. Thus the limit has to be on the sever side, not on your side.

      Feel free to leave any questions or suggestions!

      Cheers!

  1. Very good post, I like it very much. Thanks!

  1. No trackbacks yet.