In your view controller's viewDidLoad
, create the NSOperationQueue
that we'll use for downloading. Also specify how many concurrent operations your server will permit:
downloadQueue = [[NSOperationQueue alloc] init];
downloadQueue.maxConcurrentOperationCount = 4; // replace this with an appropriate value for your server
Before, you had an array, webUrls
, which was an array of NSURL
objects. In point #4, below, we'll discuss retiring that array, and create a new array of row objects. But before we can do that, we should create this new RowData
object.
Each row object will have not only the webURL
, but also other things, such as the durationText
and perhaps even the AVPlayerItem
itself. (By keeping these other object properties, when a cell scrolls back into view, we don't need to re-download the data.) So the public interface for this new class might look like:
//
// RowData.h
//
#import <Foundation/Foundation.h>
@class AVPlayerItem;
@interface RowData : NSObject
@property (nonatomic, strong) NSURL *webURL;
@property (nonatomic, strong) NSString *durationText;
@property (nonatomic, strong) AVPlayerItem *playerItem;
@property (nonatomic, getter = isDownloaded, readonly) BOOL downloaded;
@property (nonatomic, getter = isDownloading, readonly) BOOL downloading;
- (void)downloadInQueue:(NSOperationQueue *)queue completion:(void (^)(BOOL success))block;
- (void)cancelDownload;
@end
By the way, I'm not crazy about the class name, RowData
. It's a little too ambiguous. But I don't know enough about the nature of your model data to suggest a better name. Feel free to call this class whatever you think is appropriate.
Your new RowData
class can have an instance method, called downloadInQueue
, that performs the download, sets the durationText
appropriately, etc. By moving the download logic here, we successfully isolate cellForRowAtIndexPath
from some of the gory details involved with downloading. Just as importantly, though, this downloadInQueue
method won't update the user interface itself, but rather it has completion
block provided by cellForRowAtIndexPath
(demonstrated in point #5, below), so this downloadInQueue
method doesn't have to worry about UI considerations. Anyway, the implementation of downloadInQueue
might look like:
//
// RowData.m
//
#import "RowData.h"
#import <AVFoundation/AVFoundation.h>
@interface RowData ()
@property (nonatomic, getter = isDownloaded) BOOL downloaded;
@property (nonatomic, getter = isDownloading) BOOL downloading;
@property (nonatomic, weak) NSOperation *operation;
@end
@implementation RowData
- (void)downloadInQueue:(NSOperationQueue *)queue completion:(void (^)(BOOL success))completion
{
if (!self.isDownloading)
{
self.downloading = YES;
NSOperation *currentOperation = [NSBlockOperation blockOperationWithBlock:^{
BOOL success = NO;
self.playerItem = [AVPlayerItem playerItemWithURL:self.webURL];
if (self.playerItem)
{
success = YES;
CMTime timeduration = self.playerItem.duration;
float seconds = CMTimeGetSeconds(timeduration);
self.durationText = [NSString stringWithFormat:@"%f", seconds];
}
self.downloading = NO;
self.downloaded = YES;
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
completion(success);
}];
}];
[queue addOperation:currentOperation];
self.operation = currentOperation;
}
}
- (void)cancelDownload
{
if ([self isDownloading] && self.operation)
{
self.downloading = NO;
[self.operation cancel];
}
}
@end
In your main view controller, rather than creating your old array of webUrls
, create a new array of these RowData
objects called, for example, objects
. Set the webURL
property for each of those RowData
objects, of course. (Again, I'm not crazy about the ambiguous name of objects
, but I don't know enough about your app to make a more specific suggestion. Call this whatever you want. But my code below will use objects
.)
Finally, modify your cellForRowAtIndexPath
to use this new RowData
object and it's downloadInQueue
method. Also, note, the completion
block checks to make sure the cell is still visible:
RowData *rowData = self.objects[indexPath.row];
if ([rowData isDownloaded])
{
cell.detailTextLabel.text = rowData.durationText;
}
else
{
cell.detailTextLabel.text = @"..."; // you really should initialize this so we show something during download or remove anything previously there
[rowData downloadInQueue:self.downloadQueue completion:^(BOOL success) {
// note, if you wanted to change your behavior based upon whether the
// download was successful or not, just use the `success` variable
UITableViewCell *updateCell = [tblView cellForRowAtIndexPath:indexPath];
// by the way, make sure the cell is still on screen
if (updateCell)
{
updateCell.detailTextLabel.text = rowData.durationText;
[updateCell setNeedsLayout];
}
}];
}
If using iOS 6, if you want to cancel pending downloads when a cell scrolls off the screen, you can use the didEndDisplayingCell
method of the UITableViewDelegate
protocol.
- (void)tableView:(UITableView *)tableView didEndDisplayingCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath
{
RowData *rowData = self.objects[indexPath.row];
if ([rowData isDownloading])
[rowData cancelDownload];
}
If supporting earlier versions of iOS, you'd have to use UIScrollViewDelegate
protocol methods, such as scrollViewDidScroll
, determine which cells have scrolled off the screen (e.g. are not included in indexPathsForVisibleRows
) manually, but the idea is the same.