Why is UITableView not reloading (even on the main thread)?
- by radesix
I have two programs that basically do the same thing. They read an XML feed and parse the elements. The design of both programs is to use an asynchronous NSURLConnection to get the data then to spawn a new thread to handle the parsing. As batches of 5 items are parsed it calls back to the main thread to reload the UITableView.
My issue is it works fine in one program, but not the other. I know that the parsing is actually occuring on the background thread and I know that [tableView reloadData] is executing on the main thread; however, it doesn't reload the table until all parsing is complete. I'm stumped. As far as I can tell... both programs are structured exactly the same way. Here is some code from the app that isn't working correctly.
- (void)startConnectionWithURL:(NSString *)feedURL feedList:(NSMutableArray *)list {
self.feedList = list;
// Use NSURLConnection to asynchronously download the data. This means the main thread will not be blocked - the
// application will remain responsive to the user.
//
// IMPORTANT! The main thread of the application should never be blocked! Also, avoid synchronous network access on any thread.
//
NSURLRequest *feedURLRequest = [NSURLRequest requestWithURL:[NSURL URLWithString:feedURL]];
self.bloggerFeedConnection = [[[NSURLConnection alloc] initWithRequest:feedURLRequest delegate:self] autorelease];
// Test the validity of the connection object. The most likely reason for the connection object to be nil is a malformed
// URL, which is a programmatic error easily detected during development. If the URL is more dynamic, then you should
// implement a more flexible validation technique, and be able to both recover from errors and communicate problems
// to the user in an unobtrusive manner.
NSAssert(self.bloggerFeedConnection != nil, @"Failure to create URL connection.");
// Start the status bar network activity indicator. We'll turn it off when the connection finishes or experiences an error.
[UIApplication sharedApplication].networkActivityIndicatorVisible = YES;
}
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
self.bloggerData = [NSMutableData data];
}
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
[bloggerData appendData:data];
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
self.bloggerFeedConnection = nil;
[UIApplication sharedApplication].networkActivityIndicatorVisible = NO;
// Spawn a thread to fetch the link data so that the UI is not blocked while the application parses the XML data.
//
// IMPORTANT! - Don't access UIKit objects on secondary threads.
//
[NSThread detachNewThreadSelector:@selector(parseFeedData:) toTarget:self withObject:bloggerData];
// farkData will be retained by the thread until parseFarkData: has finished executing, so we no longer need
// a reference to it in the main thread.
self.bloggerData = nil;
}
If you read this from the top down you can see when the NSURLConnection is finished I detach a new thread and call parseFeedData.
- (void)parseFeedData:(NSData *)data {
// You must create a autorelease pool for all secondary threads.
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
self.currentParseBatch = [NSMutableArray array];
self.currentParsedCharacterData = [NSMutableString string];
self.feedList = [NSMutableArray array];
//
// It's also possible to have NSXMLParser download the data, by passing it a URL, but this is not desirable
// because it gives less control over the network, particularly in responding to connection errors.
//
NSXMLParser *parser = [[NSXMLParser alloc] initWithData:data];
[parser setDelegate:self];
[parser parse];
// depending on the total number of links parsed, the last batch might not have been a "full" batch, and thus
// not been part of the regular batch transfer. So, we check the count of the array and, if necessary, send it to the main thread.
if ([self.currentParseBatch count] > 0) {
[self performSelectorOnMainThread:@selector(addLinksToList:) withObject:self.currentParseBatch waitUntilDone:NO];
}
self.currentParseBatch = nil;
self.currentParsedCharacterData = nil;
[parser release];
[pool release];
}
In the did end element delegate I check to see that 5 items have been parsed before calling the main thread to perform the update.
- (void)parser:(NSXMLParser *)parser didEndElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName {
if ([elementName isEqualToString:kItemElementName]) {
[self.currentParseBatch addObject:self.currentItem];
parsedItemsCounter++;
if (parsedItemsCounter % kSizeOfItemBatch == 0) {
[self performSelectorOnMainThread:@selector(addLinksToList:) withObject:self.currentParseBatch waitUntilDone:NO];
self.currentParseBatch = [NSMutableArray array];
}
}
// Stop accumulating parsed character data. We won't start again until specific elements begin.
accumulatingParsedCharacterData = NO;
}
- (void)addLinksToList:(NSMutableArray *)links {
[self.feedList addObjectsFromArray:links];
// The table needs to be reloaded to reflect the new content of the list.
if (self.viewDelegate != nil && [self.viewDelegate respondsToSelector:@selector(parser:didParseBatch:)]) {
[self.viewDelegate parser:self didParseBatch:links];
}
}
Finally, the UIViewController delegate:
- (void)parser:(XMLFeedParser *)parser didParseBatch:(NSMutableArray *)parsedBatch {
NSLog(@"parser:didParseBatch:");
[self.selectedBlogger.feedList addObjectsFromArray:parsedBatch];
[self.tableView reloadData];
}
If I write to the log when my view controller delegate fires to reload the table and when cellForRowAtIndexPath fires as it's rebuilding the table then the log looks something like this:
parser:didParseBatch:
parser:didParseBatch:
tableView:cellForRowAtIndexPath
tableView:cellForRowAtIndexPath
tableView:cellForRowAtIndexPath
tableView:cellForRowAtIndexPath
tableView:cellForRowAtIndexPath
parser:didParseBatch:
parser:didParseBatch:
parser:didParseBatch:
tableView:cellForRowAtIndexPath
tableView:cellForRowAtIndexPath
tableView:cellForRowAtIndexPath
tableView:cellForRowAtIndexPath
tableView:cellForRowAtIndexPath
parser:didParseBatch:
tableView:cellForRowAtIndexPath
tableView:cellForRowAtIndexPath
tableView:cellForRowAtIndexPath
tableView:cellForRowAtIndexPath
tableView:cellForRowAtIndexPath
parser:didParseBatch:
parser:didParseBatch:
parser:didParseBatch:
parser:didParseBatch:
tableView:cellForRowAtIndexPath
tableView:cellForRowAtIndexPath
tableView:cellForRowAtIndexPath
tableView:cellForRowAtIndexPath
tableView:cellForRowAtIndexPath
Clearly, the tableView is not reloading when I tell it to every time.
The log from the app that works correctly looks like this:
parser:didParseBatch:
tableView:cellForRowAtIndexPath
tableView:cellForRowAtIndexPath
tableView:cellForRowAtIndexPath
tableView:cellForRowAtIndexPath
tableView:cellForRowAtIndexPath
parser:didParseBatch:
tableView:cellForRowAtIndexPath
tableView:cellForRowAtIndexPath
tableView:cellForRowAtIndexPath
tableView:cellForRowAtIndexPath
tableView:cellForRowAtIndexPath
parser:didParseBatch:
tableView:cellForRowAtIndexPath
tableView:cellForRowAtIndexPath
tableView:cellForRowAtIndexPath
tableView:cellForRowAtIndexPath
tableView:cellForRowAtIndexPath
parser:didParseBatch:
tableView:cellForRowAtIndexPath
tableView:cellForRowAtIndexPath
tableView:cellForRowAtIndexPath
tableView:cellForRowAtIndexPath
tableView:cellForRowAtIndexPath
parser:didParseBatch:
tableView:cellForRowAtIndexPath
tableView:cellForRowAtIndexPath
tableView:cellForRowAtIndexPath
tableView:cellForRowAtIndexPath
tableView:cellForRowAtIndexPath