Index '5' beyond bounds of empty array crash

3.2k views Asked by At

I'm building an RSS-Reader and put a refreshbutton on the right corner of the navigation bar. It works fine and I get no crashes. But if if I press the refresh button during scrolling the app crashes. And I have no idea where the problem is. I analyzed the project but it couldn't find anything...

So here's the error I get:

2012-01-22 16:36:48.205 GYSA[712:707] *** Terminating app due to uncaught exception 'NSRangeException', reason: '*** -[__NSArrayM objectAtIndex:]: index 5 beyond bounds for empty array'
*** First throw call stack:
(0x37adb8bf 0x315c11e5 0x37a24b6b 0x7913 0x34ef39cb 0x34ef2aa9 0x34ef2233 0x34e96d4b 0x37a3a22b 0x33231381 0x33230f99 0x3323511b 0x33234e57 0x3325c6f1 0x3327f4c5 0x3327f379 0x37249f93 0x3747b891 0x37aa4f43 0x37aaf553 0x37aaf4f5 0x37aae343 0x37a314dd 0x37a313a5 0x375affcd 0x34ec1743 0x2ac9 0x2a54)
terminate called throwing an exception(gdb)

And here's my code:

#import "RssFunViewController.h"
#import "BlogRssParser.h"
#import "BlogRss.h"

@implementation RssFunViewController

@synthesize rssParser = _rssParser;
@synthesize tableView = _tableView;
@synthesize appDelegate = _appDelegate;
@synthesize toolbar = _toolbar;

-(void)toolbarInit{
    UIBarButtonItem *refreshButton = [[UIBarButtonItem alloc]
                                   initWithBarButtonSystemItem:UIBarButtonSystemItemRefresh
                                   target:self action:@selector(reloadRss)];
    refreshButton.enabled = YES;
    self.navigationItem.rightBarButtonItem = refreshButton;
    [refreshButton release];
    UIImage *image = [UIImage imageNamed: @"navigationbar.png"];
    UIImageView *imageview = [[UIImageView alloc] initWithImage: image];

    UIBarButtonItem *button = [[UIBarButtonItem alloc] initWithCustomView: imageview];
    self.navigationItem.leftBarButtonItem = button;
    [imageview release];
    [button release];
}


// Implement viewDidLoad to do additional setup after loading the view, typically from a nib.
- (void)viewDidLoad {

    [super viewDidLoad];
    self.view.autoresizesSubviews = YES;
    self.view.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
    [self toolbarInit];
    _rssParser = [[BlogRssParser alloc]init];
    self.rssParser.delegate = self;
    [[self rssParser]startProcess];
}

-(void)reloadRss{
    [self toggleToolBarButtons:NO];
    [[self rssParser]startProcess];
}

-(void)toggleToolBarButtons:(BOOL)newState{
    NSArray *toolbarItems = self.toolbar.items;
    for (UIBarButtonItem *item in toolbarItems){
        item.enabled = newState;
    }   
}

//Delegate method for blog parser will get fired when the process is completed
- (void)processCompleted{
    //reload the table view
    [self toggleToolBarButtons:YES];
    [[self tableView]reloadData];
}

-(void)processHasErrors{
    //Might be due to Internet
    UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Achtung!" message:@"Leider ist es im Moment nicht möglich eine Verbindung zum Internet herzustellen. Ohne Internetverbindung ist die App nur in beschränktem Umfang nutzbar!"
                                                   delegate:nil cancelButtonTitle:@"OK" otherButtonTitles: nil];
    [alert show];   
    [alert release];
    [self toggleToolBarButtons:YES];
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{
    return [[[self rssParser]rssItems]count];
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
    UITableViewCell * cell = [tableView dequeueReusableCellWithIdentifier:@"rssItemCell"];
    if(nil == cell){
        cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:@"rssItemCell"]autorelease];
    }
    cell.textLabel.text = [[[[self rssParser]rssItems]objectAtIndex:indexPath.row]title];
    cell.detailTextLabel.text = [[[[self rssParser]rssItems]objectAtIndex:indexPath.row]description];
    cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
    return cell;
}

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
    [[self appDelegate] setCurrentlySelectedBlogItem:[[[self rssParser]rssItems]objectAtIndex:indexPath.row]];
    [self.appDelegate loadNewsDetails];
    [_tableView deselectRowAtIndexPath:indexPath animated: YES];
}

- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation
{
    // Return YES for supported orientations
    return (interfaceOrientation != UIInterfaceOrientationPortraitUpsideDown);
}

- (void)dealloc {
    [_appDelegate release];
    [_toolbar release];
    [_tableView release];
    [_rssParser release];
    [super dealloc];
}

@end

I found the code line that causes the problem:

cell.textLabel.text = [[[[self rssParser]rssItems]objectAtIndex:indexPath.row]title];
cell.detailTextLabel.text = [[[[self rssParser]rssItems]objectAtIndex:indexPath.row]description];

If I delete these codelines I can't reproduce the error. But they're necessary for the RSS feed as you can imagine :).

Any solutions?

Here's the fetching code:

#import "BlogRssParser.h"
#import "BlogRss.h"

@implementation BlogRssParser

@synthesize currentItem = _currentItem;
@synthesize currentItemValue = _currentItemValue;
@synthesize rssItems = _rssItems;
@synthesize delegate = _delegate;
@synthesize retrieverQueue = _retrieverQueue;


- (id)init{
    self = [super init];
    if(self){
        _rssItems = [[NSMutableArray alloc]init];
    }
    return self;
}

- (NSOperationQueue *)retrieverQueue {
    if(nil == _retrieverQueue) {
        _retrieverQueue = [[NSOperationQueue alloc] init];
        _retrieverQueue.maxConcurrentOperationCount = 1;
    }
    return _retrieverQueue;
}

- (void)startProcess{
    SEL method = @selector(fetchAndParseRss);
    [[self rssItems] removeAllObjects];
    NSInvocationOperation *op = [[NSInvocationOperation alloc] initWithTarget:self 
                                                                     selector:method 
                                                                       object:nil];
    [self.retrieverQueue addOperation:op];
    [op release];
}

-(BOOL)fetchAndParseRss{
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];

    [UIApplication sharedApplication].networkActivityIndicatorVisible = YES;

    //To suppress the leak in NSXMLParser
    [[NSURLCache sharedURLCache] setMemoryCapacity:0];
    [[NSURLCache sharedURLCache] setDiskCapacity:0];

    BOOL success = NO;
    NSXMLParser *parser = [[NSXMLParser alloc] initWithContentsOfURL:url];
    [parser setDelegate:self];
    [parser setShouldProcessNamespaces:YES];
    [parser setShouldReportNamespacePrefixes:YES];
    [parser setShouldResolveExternalEntities:NO];
    success = [parser parse];
    [parser release];
    [pool drain];
    return success;
}

- (void)parser:(NSXMLParser *)parser didStartElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI 
 qualifiedName:(NSString *)qualifiedName attributes:(NSDictionary *)attributeDict{
    if(nil != qualifiedName){
        elementName = qualifiedName;
    }
    if ([elementName isEqualToString:@"item"]) {
        self.currentItem = [[[BlogRss alloc]init]autorelease];
    }else if ([elementName isEqualToString:@"media:thumbnail"]) {
        self.currentItem.mediaUrl = [attributeDict valueForKey:@"url"];
    } else if([elementName isEqualToString:@"title"] || 
              [elementName isEqualToString:@"description"] ||
              [elementName isEqualToString:@"link"] ||
              [elementName isEqualToString:@"guid"] ||
              [elementName isEqualToString:@"pubDate"]) {
        self.currentItemValue = [NSMutableString string];
    } else {
        self.currentItemValue = nil;
    }   
}

- (void)parser:(NSXMLParser *)parser didEndElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName {
    if(nil != qName){
        elementName = qName;
    }
    if([elementName isEqualToString:@"title"]){
        self.currentItem.title = self.currentItemValue;
    }else if([elementName isEqualToString:@"description"]){
        self.currentItem.description = self.currentItemValue;
    }else if([elementName isEqualToString:@"link"]){
        self.currentItem.linkUrl = self.currentItemValue;
    }else if([elementName isEqualToString:@"guid"]){
        self.currentItem.guidUrl = self.currentItemValue;
    }else if([elementName isEqualToString:@"pubDate"]){
        NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
        [formatter setDateFormat:@"yyyy-MM-dd'T'HH:mm:ss'Z'"];
        self.currentItem.pubDate = [formatter dateFromString:self.currentItemValue];
        [formatter release];
    }else if([elementName isEqualToString:@"item"]){
        [[self rssItems] addObject:self.currentItem];
    }
}

- (void)parser:(NSXMLParser *)parser foundCharacters:(NSString *)string {
    if(nil != self.currentItemValue){
        [self.currentItemValue appendString:string];
    }
}

- (void)parser:(NSXMLParser *)parser foundCDATA:(NSData *)CDATABlock{
    //Not needed for now
}

- (void)parser:(NSXMLParser *)parser parseErrorOccurred:(NSError *)parseError{
    if(parseError.code != NSXMLParserDelegateAbortedParseError) {
        [UIApplication sharedApplication].networkActivityIndicatorVisible = NO;
        [(id)[self delegate] performSelectorOnMainThread:@selector(processHasErrors)
         withObject:nil
         waitUntilDone:NO];
    }
}



- (void)parserDidEndDocument:(NSXMLParser *)parser {
    [(id)[self delegate] performSelectorOnMainThread:@selector(processCompleted)
     withObject:nil
     waitUntilDone:NO];
    [UIApplication sharedApplication].networkActivityIndicatorVisible = NO;
}


-(void)dealloc{
    self.currentItem = nil;
    self.currentItemValue = nil;
    self.delegate = nil;

    [_rssItems release];
    [super dealloc];
}

@end
4

There are 4 answers

3
NJones On BEST ANSWER

What you should be doing is copying your fetched data array to an ivar. Then populating your tableview from that ivar, and then in processCompleted copying the new data to the ivar and call reloadData. This will keep the the tableview from being in the inconsistent state you're experiencing.

@property (retain, nonatomic) NSArray *sourceArray;

- (void)processCompleted{
    self.sourceArray = [[[[self rssParser]rssItems] copy] autorelease];
    [self toggleToolBarButtons:YES];
    [[self tableView]reloadData];
}

And then when populating the tableview refer to the copied array. For example:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
    UITableViewCell * cell = [tableView dequeueReusableCellWithIdentifier:@"rssItemCell"];
    if(nil == cell){
        cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:@"rssItemCell"]autorelease];
    }
    cell.textLabel.text = [[self.sourceArray objectAtIndex:indexPath.row]title];
    cell.detailTextLabel.text = [[self.sourceArray objectAtIndex:indexPath.row]description];
    cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
    return cell;
}

And similarly in every other tableview delegate method where you reference [[self rssParser]rssItems].

0
prajakta kulkarni On

Check while reloading tableview. When you scroll reload tableview is automatically get called. So check is the array has values in it before assigning it's content to cell. Check array count is greter than 0 and then write these lines.

2
Giuseppe Garassino On

In my case adding this line of code at the beginning of the method called for refresh worked:

tableView.scrollEnabled = NO;

Of course you need to set your tableView again at the end:

tableView.scrollEnabled = YES;
1
Abizern On

It could be that while you are scrolling and refreshing at the same time, your datasource is being emptied, before it is being filled. So, while your tableview thinks that it has 5 rows, your datasource does not have 5 items because you are downloading them from the wherever your source is. And when it queries for the fifth item, there is nothing there and your application falls over.

Edit

I was right. Your refresh code calls startProcess which empties out the array that you use to populate the array, and then you add to it an item at a time, and you are doing this in a background queue so it's probably asynchronous.

The solution to this is to write your new items to an intermediate array in your background queue, and when the process has finished, replace your current rssItems with this array and reload your tableview.