Reputation: 135
I have a UITableViewController.
I want to call a URL (http://webservices.company.nl/api?station=ut) multiple times (for each train station) where "ut" is always different (it's the code of the station). And I want to put the results each time in a new tableview row. (The URL returns XML).
To call the URL, I use this:
// Create connection
NSURLConnection *urlConnection = [NSURLConnection connectionWithRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:[NSString stringWithFormat: @"http://webservices.company.nl/api?station=%@", station.stationCode]]] delegate:self];
[urlConnection start];
Then in "connectionDidFinishLoading" I've this for parsing the URL content with NSXMLParser:
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
NSXMLParser *parser = [[NSXMLParser alloc] initWithData:receivedDataFromURL];
[parser setDelegate:self];
[parser parse];
}
I've implemented all the methods like "didStartElement", "didEndElement" and it successfully reads all the elements in the file.
My question:
What's the best way to do this for every row in my tableview and how can I put the results in every row? I don't know what the best structure is for this, because I want to do this async.
Many thanks in advance.
Upvotes: 2
Views: 701
Reputation: 62676
The pattern here is just like lazy loading images.
1) Create a custom object like TrainStation, it should have an NSString station code, some BOOL property of function that tells callers that it's been initialized from the web service, and an init method that provides a block completion handler.
// TrainStation.h
@interface TrainStation : NSObject
@property (strong, nonatomic) NSString *stationCode; // your two character codes
@property (strong, nonatomic) id stationInfo; // stuff you get from the web service
@property (strong, nonatomic) BOOL hasBeenUpdated;
@property (copy, nonatomic) void (^completion)(BOOL);
- (void)updateWithCompletion:(void (^)(BOOL))completion;
@end
2) The completion handler starts an NSURLConnection, saving the completion block for later when the parse is done...
// TrainStation.m
- (void)updateWithCompletion:(void (^)(BOOL))completion {
self.completion = completion;
NSURL *url = // form this url using self.stationCode
NSURLRequest *request = [[NSURLRequest alloc] initWithURL:url];
[NSURLConnection sendAsynchronousRequest:self queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) {
}];
}
// TrainStation does it's own parsing, then
- (void)parserDidEndDocument:(NSXMLParser *)parser
self.hasBeenUpdated = YES;
self.completion(YES);
// when you hold a block, nil it when you're through with it
self.completion = nil;
}
3) The view controller containing the table needs to be aware that tableview cells come and go as they please, depending on scrolling, so the only safe place for the web result is the model (the array of TrainStations)
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
// normal stuff, dequeue cell, etc.
// the interesting part
TrainStation *trainStation = self.array[indexPath.row];
if ([trainStation hasBeenUpdated]) {
cell.detailTextLabel.text = [trainStation.stationInfo description];
// that's just a shortcut. teach your train station how to produce text about itself
} else { // we don't have station info yet, but we need to return from this method right away
cell.detailTextLabel.text = @"";
[trainStation updateWithCompletion:^(id parse, NSError *) {
// this runs later, after the update is finished. the block retains the indexPath from the original call
if ([[tableView indexPathsForVisibleRows] containsObject:indexPath]) {
// this method will run again, but now trigger the hasBeenUpdated branch of the conditional
[tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation: UITableViewRowAnimationAutomatic];
}
}];
}
return cell;
}
Upvotes: 2
Reputation: 437622
There are a few considerations:
You probably want to make each of these requests its own object so that you can have them running concurrently. The right approach is probably a custom operation for a NSOperationQueue
to encapsulate the downloading and parsing of the XML. A couple of considerations here:
You should make this operation so it can operate concurrently.
You should make the operation respond to cancellation events.
Note, if you do your own NSOperation
with a NSURLConnection
with your own NSURLConnectionDataDelegate
methods, you have to do some silliness with scheduling it in an appropriate run loop. I usually create a separate thread with its own runloop, but I see lots of people simply doing:
NSURLConnection *connection = [[NSURLConnection alloc] initWithRequest:request delegate:self startImmediately:NO];
[connection scheduleInRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
[connection start];
You probably want to implement a caching mechanism:
At a minimum, you want to cache responses into memory (e.g. a NSCache
) so that if you scroll down and then scroll back up, it doesn't need to reissue requests that it only just sent;
Depending upon the needs of your app, you might want a persistent storage cache, too. Maybe you don't in this particular situation, but it's a common consideration in these sorts of cases.
Given the network intense nature of your problem, I'd make sure you test your app in network realistic, real-world (and worst case) scenarios. On the simulator, you can achieve that with the "Network Link Conditioner" which is part of the "Hardware IO Tools" (available from the "Xcode" menu, choose "Open Developer Tool" - "More Developer Tools"). If you install the "Network Link Conditioner", you can then have your simulator simulate a variety of network experiences (e.g. Good 3G connection, Poor Edge connection, etc.).
Anyway, putting the this together, here is an example that is doing a XML request for every row (in this case, looking up the temperature for a city on Yahoo's weather service).
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *cellIdentifier = @"Cell";
CityCell *cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier];
// try to retrieve the cell from the cache
NSString *key = self.objects[indexPath.row];
City *cityFromCache = [self.cache objectForKey:key];
if (cityFromCache)
{
// if successful, use the data from the cache
cell.textLabel.text = cityFromCache.temperature;
cell.detailTextLabel.text = cityFromCache.name;
}
else
{
// if we have a prior operation going for this cell (i.e. for a row that has
// since scrolled off the screen), cancel it so the display of the current row
// is not delayed waiting for data for rows that are no longer visible;
// obviously, for this to work, you need a `weak` property for the operation
// in your `UITableViewCell` subclass
[cell.operation cancel];
// re-initialize the cell (so we don't see old data from dequeued cell while retrieving new data)
cell.textLabel.text = nil;
cell.detailTextLabel.text = nil;
// initiate a network request for the new data; when it comes in, update the cell
CityOperation *operation = [[CityOperation alloc] initWithWoeid:key successBlock:^(City *city) {
// see if the cell is still visible
UITableViewCell *updateCell = [tableView cellForRowAtIndexPath:indexPath];
// if the cell for this row is still visible, update it
if (updateCell)
{
updateCell.textLabel.text = city.temperature;
updateCell.detailTextLabel.text = city.name;
[updateCell setNeedsLayout];
}
// let's save the data in our cache, too
[self.cache setObject:city forKey:key];
}];
// in our custom cell subclass, I'll keep a weak reference to this operation so
// we can cancel it if I need to
cell.operation = operation;
// initiate the request
[self.queue addOperation:operation];
}
return cell;
}
In practice might move some of that logic into my cell subclass, but hopefully this illustrates the idea.
Having outlined an answer to your question, I must confess that when you described what you're trying to do, I immediately gravitated to radically different designs. E.g. I might kick off an asynchronous process that does a bunch of XML requests, updating a database, posting notification to my table view letting it know when data has been inserted. But this is a more radical departure from what you've asked, so I refrained. But it might be worthwhile to step back and consider the overall architecture.
Upvotes: 1