0

I'm creating a UITableViewController to display the roster of a hockey team. The tableViewController makes calls to the web to get the player's stats and a small picture to display in the tableViewCell. However, when I scroll through the TableView, it isn't smooth. It's incredibly jagged. How can I make it so (if this will decrease its work load) the player's pictures don't load until they're on-screen? Here is my current code (I've subclassed UITableViewCell):

EDIT: I've edited my code to follow a comment below. The property imagesCache is actually a UIMutableDictionary (confusing, sorry). However, now I get the error:

*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '*** setObjectForKey: object cannot be nil (key: http://app-assets3.sportngin.com/app_images/noPhoto-square.jpg?1428933774)'
*** First throw call stack:
(0x1865f6530 0x1975cc0e4 0x1864e1348 0x1000496a8 0x185f87168 0x1874d3be8 0x187425374 0x187414ecc 0x1874d694c 0x1000acf94 0x1000b7db8 0x1000b02c4 0x1000ba5d4 0x1000bc248 0x197dfd22c 0x197dfcef0)
libc++abi.dylib: terminating with uncaught exception of type NSException

Here is my code:

#import "RosterTableTableViewController.h"
#import "TFHpple.h"
#import "RosterListing.h"
#import "RosterListingCellTableViewCell.h"

@interface RosterTableTableViewController ()

@property (nonatomic, strong) NSMutableArray *rosters;
@property (nonatomic, strong) NSMutableDictionary *imagesDictionary;
@property NSMutableDictionary *imageCache;

@end

@implementation RosterTableTableViewController

- (void) loadRoster
{
    NSURL *RosterURL = [NSURL     URLWithString:@"http://www.lancers.com/roster/show/1502650?subseason=197271"];
    NSData *RosterHTMLData = [NSData dataWithContentsOfURL:RosterURL];

    TFHpple *RosterParser = [TFHpple hppleWithHTMLData:RosterHTMLData];

    // Get the data

    NSString *RosterNumberPathQueryString =     @"//tbody[@id='rosterListingTableBodyPlayer']/tr/td[@class='number']";
    NSArray *RosterNumberNodes = [RosterParser     searchWithXPathQuery:RosterNumberPathQueryString];
    NSString *RosterNamePathQueryString =     @"//tbody[@id='rosterListingTableBodyPlayer']/tr/td[@class='name']/a";
    NSArray *RosterNameNodes = [RosterParser     searchWithXPathQuery:RosterNamePathQueryString];
    NSString *RosterImagePathQueryString =     @"//tbody[@id='rosterListingTableBodyPlayer']/tr/td[@class='photo']/a/img";
    NSArray *RosterImageNodes = [RosterParser searchWithXPathQuery:RosterImagePathQueryString];

    NSMutableArray *rosterItems = [[NSMutableArray alloc] initWithCapacity:0];

for (int i = 0; i < RosterNumberNodes.count; ++i) {
    RosterListing *thisRosterListing = [[RosterListing alloc] init];
    thisRosterListing.playerNumber = [[[RosterNumberNodes objectAtIndex:i] firstChild] content];
    thisRosterListing.playerName = [[[RosterNameNodes objectAtIndex:i] firstChild] content];
    thisRosterListing.playerURL = [[RosterNameNodes objectAtIndex:i] objectForKey:@"href"];

        @try {
            thisRosterListing.playerImageURL = [[RosterImageNodes objectAtIndex:i] objectForKey:@"src"];
        }
        @catch (NSException *e) {}
    /*
    NSLog(@"%@", thisRosterListing.playerNumber);
    NSLog(@"%@", thisRosterListing.playerName);
    NSLog(@"%@", thisRosterListing.playerURL);
    NSLog(@"%@", thisRosterListing.playerImageURL);
    */

    [rosterItems addObject:thisRosterListing];
}

self.rosters = rosterItems;

}

- (instancetype) initWithStyle:(UITableViewStyle)style
{
self = [super initWithStyle:style];
if (self) {
    self.navigationItem.title = @"Roster";
    self.imageCache = [[NSMutableDictionary alloc] init];
}

    return self;
}


- (void)viewDidLoad {
[super viewDidLoad];

[self loadRoster];

// Load the Cell NIB file
UINib *nib = [UINib nibWithNibName:@"RosterListingCellTableViewCell" bundle:nil];

// Register this NIB, which contains the cell
[self.tableView registerNib:nib forCellReuseIdentifier:@"RosterCell"];

// Uncomment the following line to preserve selection between presentations.
// self.clearsSelectionOnViewWillAppear = NO;

// Uncomment the following line to display an Edit button in the navigation bar for this view controller.
// self.navigationItem.rightBarButtonItem = self.editButtonItem;
}

- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}

- (CGFloat) tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
        return 54;
}


- (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.
return self.rosters.count;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
// Get a new or recycled cell
RosterListingCellTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"RosterCell" forIndexPath:indexPath];

RosterListing *thisRosterListing = [self.rosters objectAtIndex:indexPath.row];
cell.playerNumberLabel.text = thisRosterListing.playerNumber;
cell.playerNameLabel.text = thisRosterListing.playerName;




__block UIImage *image = [self.imageCache objectForKey:thisRosterListing.playerImageURL];
cell.imageView.image = image;
if(image == nil) {
    //If nil it's not downloaded, so we download it,
    //We MUST download in a separate thread otherwise the scroll will be really slow cause the main queue will try to download each cell as they show up and every time they show up
    NSURL *imageURL = [NSURL URLWithString: thisRosterListing.playerImageURL];
    NSURLSessionConfiguration *sessionConfiguration = [NSURLSessionConfiguration ephemeralSessionConfiguration];
    NSURLSession *session = [NSURLSession sessionWithConfiguration:sessionConfiguration];
    NSURLSessionDataTask *dataTask = [session dataTaskWithURL:imageURL
                                            completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
                                                //Completion Handler is executed in an async way
                                                if([self.imageCache objectForKey:thisRosterListing.playerImageURL] == nil)
                                                    self.imageCache[thisRosterListing.playerImageURL] = image;
                                                //We need to execute the image update in the main queue otherwise it won't work
                                                [[NSOperationQueue mainQueue] addOperationWithBlock:^{
                                                    RosterListingCellTableViewCell *aCell = (RosterListingCellTableViewCell *)[tableView cellForRowAtIndexPath:indexPath];
                                                    aCell.imageView.image = image;
                                                }];
                                            }];

    [dataTask resume];
}

return cell;
}
Jay
  • 17
  • 1
  • 1
  • 5
  • Never, ever do network access on the main thread. – rmaddy Apr 13 '15 at 21:19
  • Why not? I only know how to single thread. – Jay Apr 13 '15 at 21:31
  • Why not? Your issue is why not. It blocks the main thread. What if the user is on a really slow network connection? The app freezes as the download tries to happen leading to terrible user experience. There are plenty of discussions here about fixing this issue. They all involve loading the images in the background so the UI isn't stuck during the downloads. – rmaddy Apr 13 '15 at 21:33
  • Can you suggest one to me? – Jay Apr 13 '15 at 21:37

1 Answers1

0

Working with images in UITableViewCells can be a bit tricky at first, I do have a code that might help you, give me a second while I search it.

Basically what you want to do is check that the row you downloaded the image for is still been displayed (as the user can scroll faster than images are downloaded) and after download ended storage it locally so you won't have to download it again.

EDIT: Here is the code, sorry the lateness

@propery NSMutableDictionary *imagesDictionary;
...
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
//...
//We load the model information for that cell
NSDictionary *cellInfo = self.dataModel[indexPath.row];

__block UIImage *image = [self.imagesDictionary objectForKey:[cellInfo objectForKey:@"avatar"]];
cell.avatarView.image = image;
if(image == nil) {
    //If nil it's not downloaded, so we download it, 
    //We MUST download in a separate thread otherwise the scroll will be really slow cause the main queue will try to download each cell as they show up and every time they show up 
    NSURL *imageURL = [NSURL URLWithString:URL];
    NSURLSessionConfiguration *sessionConfiguration = [NSURLSessionConfiguration ephemeralSessionConfiguration];
    NSURLSession *session = [NSURLSession sessionWithConfiguration:sessionConfiguration];
    NSURLSessionDataTask *dataTask = [session dataTaskWithURL:imageURL
                                            completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
       //Completion Handler is executed in an async way 
       if([self.imagesDictionary objectForKey:[cellInfo objectForKey:@"avatar"]] == nil)
                                                    if(error == nil) {
                                                      // no error
                                                      image = [UIImage imageWithData:data];
                                                    if (image == nil) {
                                                        //nil image, in my case I use a default undefined image
                                                        image = [UIImage imageNamed:@"undefined_user"];
                                                    }
                                                    //Now we are sure image is never nil
                                                    [self.imagesDictionary setObject:image forKey:[cellInfo objectForKey:@"avatar"]];
                                                    //We need to execute the image update in the main queue otherwise it won't work
                                                    [[NSOperationQueue mainQueue] addOperationWithBlock:^{
                                                            UITableviewCell *aCell = (UITableViewCell *)[tableView cellForRowAtIndexPath:indexPath];
                                                            aCell.avatarView.image = image;
                                                        }];
  }];

  [dataTask resume];

  return cell;
Benjamin Jimenez
  • 984
  • 12
  • 26
  • Ahhh. That's clever. – Jay Apr 13 '15 at 21:32
  • If you do find that code, please post it. Thank you! – Jay Apr 13 '15 at 21:55
  • Now it's posted, sorry about the delay – Benjamin Jimenez Apr 13 '15 at 22:00
  • Huh. The image isn't showing up for me. It also says that dataTask is an unused variable. Maybe that is the issue? – Jay Apr 14 '15 at 00:32
  • I added a [dataTask resume]; and it still isn't working... Dang I hope I can get this to work. Thanks again. Edit: It might have to do with the fact that I don't know what "commentInfo" is (a dictionary, I guess), so I've just replaced it with my own string. But I don't think that should really change anything. Also, of course I fixed the [[NSOperationQueue... etc.], which was accidentally commented out. – Jay Apr 14 '15 at 00:41
  • Sorry about the mistakes, I wrote it in a hurry. I made several edits to my answer, the commentInfo now cellInfo is an NSDictionary with the data model information for that cell. What issues are you experiencing now, care to update the code in your answer? – Benjamin Jimenez Apr 14 '15 at 12:27
  • Ok, I edited my code to show what I have now. I've never multithreaded before, so I apologize. I'm not sure how to fix this, but the images are just not showing up. Plus the crashing, but that's probably an easy fix. – Jay Apr 14 '15 at 15:14
  • I see now the issue. I forgot a really important line, we are not setting the downloaded data to the image, that way it will never show up, Take a special look at [UIImage imageWithData:data] – Benjamin Jimenez Apr 14 '15 at 18:48
  • Thank you tons. You're a great programmer. I ended up using a different method that solved it, but I'll have to give this one a try too. Thanks tons, again. – Jay Apr 14 '15 at 20:00