Aaron
Aaron

Reputation: 7145

How to cancel an AFHTTPRequestOperation and not retain previous progress

I have an app that is used to test the AFNetworking API. It downloads documents from a server and places them in the app's sand box. The user may start a download, pause/resume the download and cancel the download. Starting, pausing and resuming all work as expected. However cancellation does a few things I would not expect.

The App

Each cell in the table represents a "download" which is my model. The table view controller listens for taps in the cells and sends messages to begin/cancel/pause/resume downloading. I have a class DownloadManager that keeps track of my download model objects. I have a third class AFFileDownloadAPIClient (using the AFHTTPClient pattern recommended by AFNetworking authors). DownloadManager calls the appropriate messages on AFFileDownloadAPIClient which in turn calls the appropriate method on the NSOperation.

A simple AFNetworking download tracking app

The Code

The method below creates a new AFHTTPRequestOperation, streams the bits to a file (this is working fine), and throws it in a queue which starts the operation for me. A couple things to note: 1) I have put some meta data in the "Content-Disposition" header like the content length and the resulting file name, because neither are known when the download begins. Remember the bits are being streamed to me. 2) AFFileDownloadAPIClient keeps a dictionary with integer index keys and the AFHTTPRequestOperation for each download that corresponds to the index in the UITableView. I found this necessary to retrieve the operation later to pause, resume, etc...

This is in AFFileDownloadAPIClient:

- (void)downloadFileWithIndex:(int)index fileName:(NSString *)fileName {

// Using standard request operation
AFHTTPRequestOperation *operation = [[AFHTTPRequestOperation alloc] initWithRequest:request];

operation.inputStream = [NSInputStream inputStreamWithURL:request.URL];
operation.outputStream = [NSOutputStream outputStreamToFileAtPath:fileInDocumentsPath append:YES];

 // BLOCK level variables //
 __weak AFHTTPRequestOperation *weakOperation = operation;   // For use in download progress BLOCK
 __weak NSDate *startTime = [NSDate date];

[operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) {

    NSHTTPURLResponse *response = (NSHTTPURLResponse*)weakOperation.response;
    NSString *contentDisposition = [[response allHeaderFields] objectForKey:@"Content-Disposition"];
    NSArray *dispositionMetadata = [contentDisposition componentsSeparatedByString:@";"];

    NSString *fileName = @"<?>";

    // 3rd item is file name
    if (dispositionMetadata != nil && dispositionMetadata.count == 4)
    {
        fileName = [dispositionMetadata objectAtIndex:2];
    }

    if ([_downloadFileRequestDelegate respondsToSelector:@selector(downloadFileRequestFinishedWithData:fileName:atIndex:startTime:)])
        [_downloadFileRequestDelegate downloadFileRequestFinishedWithData:responseObject fileName:fileName atIndex:index startTime:startTime];

}

failure:^(AFHTTPRequestOperation *operation, NSError *error) {

        if ([_downloadFileRequestDelegate respondsToSelector:@selector(downloadFileRequestFailedWithError:atIndex:startTime:)])
            [_downloadFileRequestDelegate downloadFileRequestFailedWithError:error atIndex:index startTime:startTime];
    }
];

// Check "Content-Disposition" for content-length    
[operation setDownloadProgressBlock:^(NSUInteger bytesRead, long long totalBytesRead, long long totalBytesExpectedToRead) {

    NSHTTPURLResponse *response = (NSHTTPURLResponse*)weakOperation.response;
    NSString *contentDisposition = [[response allHeaderFields] objectForKey:@"Content-Disposition"];
    NSArray *dispositionMetadata = [contentDisposition componentsSeparatedByString:@";"];

    // 4th item is length
    if (dispositionMetadata != nil && dispositionMetadata.count == 4)
    {
        totalBytesExpectedToRead = [[dispositionMetadata objectAtIndex:3] doubleValue];
    }

    // Notify the delegate of the progress
    if ([_requestProgressDelegate respondsToSelector:@selector(requestDidReceiveBytesForIndex:bytes:totalBytes:)])
        [_requestProgressDelegate requestDidReceiveBytesForIndex:index bytes:bytesRead totalBytes:totalBytesExpectedToRead];
}];

// Check to see if operation is already in our dictionary 
if ([[self.downloadOperations allKeys] containsObject:[NSNumber numberWithInt:index]] == YES)
    [self.downloadOperations removeObjectForKey:[NSNumber numberWithInt:index]];

// Add operation to storage dictionary
[self.downloadOperations setObject:operation forKey:[NSNumber numberWithInt:index]];

// Queue up the download operation. No need to start the operation explicitly
[self enqueueHTTPRequestOperation:operation];

}

Now the cancel,pause,resume methods. Remember, pause and resume functionality seem to work just fine.

- (void)cancelDownloadForIndex:(int)index {

AFHTTPRequestOperation *operation = [self.downloadOperations objectForKey:[NSNumber numberWithInt:index]];

if (operation != nil) {

    [operation cancel];

    // Remove object from dictionary
    [self.downloadOperations removeObjectForKey:[NSNumber numberWithInt:index]];
}
}

- (void)pauseDownloadForIndex:(int)index {

AFHTTPRequestOperation *operation = [self.downloadOperations objectForKey:[NSNumber numberWithInt:index]];

if (operation != nil)
    [operation pause];
}

- (void)resumeDownloadForIndex:(int)index {

AFHTTPRequestOperation *operation = [self.downloadOperations objectForKey:[NSNumber numberWithInt:index]];

if (operation != nil)
    [operation resume];
}

The Problem

Let's say we want to cancel a download half way through. I would tap "GO" then wait a few seconds. Then tap "X" to cancel. Below is a before / after image. (Before on the left, after on the right).

Before/after download and cancel operations

After you tap "X" the view changes to show the original "GO" button so you can try again, I call this the ready state (or "before") in this case. What I don't understand is that when I tap "GO" a second time on the same download that was just cancelled, my progress indicator picks up right where the original left off at 1.98 MB.... It's as if the cancel didn't delete original bytes downloaded, remembers them and continues where it left off. Why?

Before/after download and cancel operations

Questions

  1. Why does the download after cancellation continue where it left off?
  2. Is this behavior expected or unexpected?

I apologize for the lengthy post and thank you for reading this far....

[EDIT 1]

In order to update the progressView in the UITableViewCell I have to do at least these two things.

  1. Use a data model class which I call Download.
  2. Use a data model manager class that managed my model Download objects, and their state.

In the table view controller I listen for bytes received for a given download at a given index:

- (void)downloadDidReceiveBytesForIndex:(int)downloadIndex bytes:(long long)bytes totalBytes:(double)totalBytes {

NSIndexPath *path = [NSIndexPath indexPathForRow:downloadIndex inSection:0];

DownloadTableViewCell *cell = (DownloadTableViewCell*)[self.tableView cellForRowAtIndexPath:path];

Download *download = [_downloadManager.downloads objectAtIndex:path.row];
download.bytesDownloaded += bytes;
download.percentageDownloaded = download.bytesDownloaded / totalBytes;

// as a factor of 0.0 to 1.0 not 100.
cell.downloadProgressView.progress = download.percentageDownloaded;

float MB_CONVERSION_FACTOR = 0.000000953674;

NSString *bytesText = [NSString stringWithFormat:@"Downloading %.2f of %.2f MB", roundf((download.bytesDownloaded * MB_CONVERSION_FACTOR)*100)/100.0, roundf((totalBytes * MB_CONVERSION_FACTOR)*100)/100.0];

cell.downloadProgressLabel.text = bytesText;
}

Finally, in order to handle scrolling through the table and the re-use of UITableViewCell objects. I have to ensure that my cells are created correctly, correspond to the correct download (at a given index) and reflect the downloads exact state. Some of this might be overkill, but it seems to work well. I haven't tested this in instruments to see if/when I'm leaking anything, though:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = @"Cell";
DownloadTableViewCell *cell = (DownloadTableViewCell*)[tableView dequeueReusableCellWithIdentifier:CellIdentifier];

if (cell == nil) {

    UIViewController *temporaryController = [[UIViewController alloc] initWithNibName:@"DownloadTableViewCell" bundle:nil];
    // Grab a pointer to the custom cell.
    cell = (DownloadTableViewCell *)temporaryController.view;

    [cell initState];

    // Listens for method calls on cell
    cell.delegate = self;

    cell.selectionStyle = UITableViewCellSelectionStyleNone;
}

// Set index for this cell (it could be wrong if cell is re-used)
cell.downloadIndex = indexPath.row;

Download *download = [_downloadManager.downloads objectAtIndex:indexPath.row];
cell.downloading = download.downloading;

cell.nameLabel.text = download.name;
cell.descriptionLabel.text = download.description;
cell.downloadProgressView.progress = download.percentageDownloaded;

// Check for completed status
cell.completed = download.completed;
cell.completedFileNameLabel.text = download.fileName;

return cell;
}

Upvotes: 0

Views: 1154

Answers (1)

danh
danh

Reputation: 62676

It appears as if the outputStream is being released when removed from the collection in the cancel download method. However, no state change is made on the Download instance as the download is canceled. If that object continues to have totalBytes and percentageDownloaded values set, the progress view will continue to reflect a partially downloaded state.

Upvotes: 1

Related Questions