How To Make An Interface With Horizontal Tables Like The Pulse News App: Part 2

This is a blog post by iOS Tutorial Team member Felipe Laso, an independent iOS developer and aspiring game designer/programmer. This is the second of a two-part series on how to create horizontal table views in an interface similar to the Pulse News App. If you haven’t already, check out part 1 of the tutorial […] By Ray Wenderlich.

Leave a rating/review
Save for later
Share
You are currently viewing page 4 of 5 of this article. Click here to view the first page.

Contents

Hide contents

Fixing The Duplicate Rows

This fix is not as obvious as the title label. Because of how the table view API works regarding reusable cells when we scroll our table and a cell is about to appear, we ask the system for a reusable cell from the cache and we replace it’s data accordingly.

When the user scrolls this process is invisible and you see different table cells as you scroll. But since we have a table view embedded within each row of our vertical table, we are refreshing the vertical cells but our horizontal table view doesn’t get refreshed.

One solution would be to call reloadData at the end of ArticleListViewController_iPhone.m’s cellForRowAtIndexPath method. The problem with this is that reload data reloads absolutely everything, meaning the cells that are visible as well as the cells that aren’t visible.

This method is useful when you change the data of the table, for example if we were loading our news from an RSS feed, when the user refreshes the articles and we download new data, we need to call the reloadData method.

Since we have several rows each with many images and custom content within, reloading everything within the table is overkill, and if you do reload the data you will notice stuttering and scrolling that isn’t smooth.

Not what Apple recommends and definitely not a good user experience!

The simplest solution is to create an array of horizontalTableCell cells that we can just create when we first load our app and then reuse those cells instead of reloading the entire data of the table.

Let’s just straight to it! Again this is not complicated at all.

Open the ArticleListViewController.h file and add the following instance variable and property to your class declaration:

@interface ArticleListViewController : UITableViewController
{
    NSDictionary *_articleDictionary;
    NSMutableArray *_reusableCells;
}

@property (nonatomic, retain) NSDictionary *articleDictionary;
@property (nonatomic, retain) NSMutableArray *reusableCells;

@end

We just created an NSMutableArray called reusableCells that will store our horizontalTableCell instances for reuse.

Moving over to the ArticleListViewController.m file we now have to synthesize the variable and properly release its memory:

// Right below the @implementation line
@synthesize reusableCells = _reusableCells;

// Inside dealloc and viewDidUnload
self.reusableCells = nil;

In order for this to work we must create a new mutable array and fill it with HorizontalTableCell instances so that our table view can use them when necessary.

However, we have to write our initialization code within the ArticleListViewController_iPhone.m’s viewDidLoad method and not on the standard ArticleListViewController, that’s because we want to create instances of HorizontalTableCell_iPhone for the iPhone and, later on, HorizontalTableCell_iPad for the iPad version.

Jump over to ArticleListViewController_iPhone.m and add the following code to your viewDidLoad method:

- (void)viewDidLoad
{
    [super viewDidLoad];
    
    if (!self.reusableCells)
    {       
        NSSortDescriptor* sortDescriptor = [NSSortDescriptor sortDescriptorWithKey:nil ascending:YES selector:@selector(localizedCompare:)];
        NSArray* sortedCategories = [self.articleDictionary.allKeys sortedArrayUsingDescriptors:[NSArray arrayWithObject:sortDescriptor]];
        
        NSString *categoryName;
        NSArray *currentCategory;
        
        self.reusableCells = [NSMutableArray array];
        
        for (int i = 0; i < [self.articleDictionary.allKeys count]; i++)
        {                        
            HorizontalTableCell_iPhone *cell = [[HorizontalTableCell_iPhone alloc] initWithFrame:CGRectMake(0, 0, 320, 416)];
            
            categoryName = [sortedCategories objectAtIndex:i];
            currentCategory = [self.articleDictionary objectForKey:categoryName];
            cell.articles = [NSArray arrayWithArray:currentCategory];
            
            [self.reusableCells addObject:cell];
            [cell release];
        }
    }
}

When we first enter our viewDidLoad method we call our superclass’ viewDidLoad, just standard procedure here. Afterwards we check to see if we already have created our reusable cells or not, in case we haven’t we proceed with the creation and initialization.

As you have seen before we create an NSSortDescriptor to sort our categories and store all of the sorted dictionary keys in an array. The reason for this is so that our reusableCells array contains cells already sorted in the same order as our categories, that way we just get the object at the necessary index and no further process needs to be done.

Next up we declare a categoryName string and a currentCategory array, to variables that will be useful in our loop coming up next.

In our loop we are just going to go over all of the possible dictionary keys (for each category) and create a reusable cell for each. Inside the loop we create a HorizontalTableCell_iPhone instance, initialize it with a top left position and a width and height (don’t worry about this since we overrode the initWithFrame method and it will pass in the proper frame size).

After that we just get the category array for this corresponding cell, store it within it’s articles property, add the cell we just created to our reusable cells array and release it (remember that by default when you pass add an object to an array it retains it).

Here’s where the cool part comes in, whilst still inside the ArticleListViewController_iPhone.m file, go to the cellForRowAtIndexPath method and replace it with the following code:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{        
    HorizontalTableCell *cell = [self.reusableCells objectAtIndex:indexPath.section];
    
    return cell;
}

Yup, that’s all we need. Since we already created a cache of reusable cells, all we need to do is fetch the appropriate one.

Let’s perform some cleanup now, go to the HorizontalTableCell.m file and delete the reuseIdentifier method. Since we are no longer using the provided method for reusable cells, we don’t need an identifier for them.

We are almost done!!! But before let’s build and run our project to see what we get:

Fix due to table view cell reuse - now showing correct info.

Yay!!! Our interface works to perfection :) pretty cool huh?

Now for a final performance tweak we are going to get a little help from GCD (Grand Central Dispatch).

Note: For more information on GCD and a more in depth look check out the Multithreading And Grand Central Dispatch On iOS For Beginners Tutorial available right here at Ray’s website!

Go over to the HorizontalTableCell_iPhone.m file and find your cellForRowAtIndexPath method, add the following code:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    static NSString *CellIdentifier = @"ArticleCell";
    
    __block ArticleCell_iPhone *cell = (ArticleCell_iPhone *)[tableView dequeueReusableCellWithIdentifier:CellIdentifier];
    
    if (cell == nil) 
    {
        cell = [[[ArticleCell_iPhone alloc] initWithFrame:CGRectMake(0, 0, kCellWidth, kCellHeight)] autorelease];
    }
        
    __block NSDictionary *currentArticle = [self.articles objectAtIndex:indexPath.row];
    
    dispatch_queue_t concurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    
    dispatch_async(concurrentQueue, ^{        
        UIImage *image = nil;        
        image = [UIImage imageNamed:[currentArticle objectForKey:@"ImageName"]];
        
        dispatch_async(dispatch_get_main_queue(), ^{
            [cell.thumbnail setImage:image]; 
        });
    }); 
    
    cell.titleLabel.text = [currentArticle objectForKey:@"Title"];
    
    return cell;
}

We have made a few changes to the regular method so let’s go over them one at a time.

First up we added __block prior to our ArticleCell_iPhone cell variable, that’s going to allow us to use the cell variable inside blocks. We continue with the standard check to see if we can dequeue a reusable cell or if we have to create one.

After that we create a new dispatch queue variable and as GCD to give us one of the available queues.

The interesting part is inside the dispatch_async method, all calls and process regarding UI must be performed on the main thread, but we want to load our images asynchronously in a background thread so that when our users open the app they can continue to scroll and navigate without having to wait a few seconds until all of the images are loaded.

In order to do this we use dispatch_async to run the UIImage creation in the background. Once that is done we go ahead and dispatch another asynchronous process but this time we use dispatch_get_main_queue() to get the main thread (remember that we can’t make changes to the interface on background threads) and set the thumbnail image to the one we just loaded asynchronously.

Outside of the dispatch blocks we set the title text as usual because we don’t need to load that, just read it from our array of articles.

AND WE ARE DONE! Run your app one more time and you might notice a smoother load and scroll right from the start (if not wait until you try this with 100 images, it truly makes a difference!)

:D

Congratulations on making it this far, the second part definitely involved fewer work designing interfaces and more under the hood code. In order to keep this tutorial nice and simple we will not be going over the iPad process even though it’s exactly the same (don’t worry it’s included within the source code), you can do that as homework in order to practice and further understand the project! :)

Contributors

Over 300 content creators. Join our team.