Search code examples
iosmacosios7osx-mavericksnsprogress

Using NSProgress to find download speed and time remaining


According to the documentation for NSProgress I see that -[NSProgress localizedAdditionalDescription] can report download speed and time remaining, e.g.:

1.61 GB of 3.22 GB (2 KB/sec) — 2 minutes remaining

However, I'm not able to get those details when I associate an NSProgress to a NSURLSessionDownloadTask. Here's my code:

Downloader.h

@interface Downloader : NSObject
@property NSProgress *overallProgress;
-(void)startDownload;
@end

Downloader.m

- (void)startDownload {

    self.overallProgress = [NSProgress progressWithTotalUnitCount:100];

    [self.overallProgress setKind:NSProgressKindFile];
    [self.overallProgress setUserInfoObject:NSProgressFileOperationKindKey forKey:NSProgressFileOperationKindDownloading];


    [self.overallProgress becomeCurrentWithPendingUnitCount:100];
    [self work1];
    [self.overallProgress resignCurrent];
}

- (void)work1 {

    NSProgress *firstTaskProgress = [NSProgress progressWithTotalUnitCount:1];
    [firstTaskProgress setKind:NSProgressKindFile];
    [firstTaskProgress setUserInfoObject:NSProgressFileOperationKindKey forKey:NSProgressFileOperationKindDownloading];

    NSURL *downloadURL = [NSURL URLWithString:@"http://ipv4.download.thinkbroadband.com/200MB.zip"];
    NSURL *destinationDirectory = [[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] firstObject];
    NSURL *destinationURL = [destinationDirectory URLByAppendingPathComponent:[downloadURL lastPathComponent]];

    NSURLSession *session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]];
    NSURLSessionDownloadTask *fileDownloadTask =
    [session downloadTaskWithURL:downloadURL
               completionHandler:^(NSURL *location, NSURLResponse *response, NSError *error){

                   [[NSFileManager defaultManager] removeItemAtURL:destinationURL error:NULL];

                   [[NSFileManager defaultManager] moveItemAtURL:location toURL:destinationURL error:nil];

                   [firstTaskProgress setCompletedUnitCount:1];
               }];

    [fileDownloadTask resume];
}

DownloadObserver.m

-(void)viewDidAppear:(BOOL)animated{
    [super viewDidAppear:animated];

    downloader = [Downloader new];

    [downloader addObserver:self
                 forKeyPath:@"overallProgress.fractionCompleted"
                    options:NSKeyValueObservingOptionNew
                    context:NULL];

    [downloader startDownload];

}

-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
    NSLog(@"%@", [downloader.overallProgress localizedAdditionalDescription]);
}

This only prints out:

0 of 100

Zero KB of 100 bytes

How can I get localizedAdditionalDescription to print the download speed and time remaining?


Solution

  • First of all, totalUnitCount should correspond to the size of a file in bytes. During download process we change completedUnitCount which reflects on localizedAdditionalDescription. NSProgressKindFile is a hint to format those values as file size.

    To set progress properly we need to use NSURLSessionDownloadDelegate methods instead of block handler. In -URLSession: downloadTask: didWriteData: totalBytesWritten: totalBytesExpectedToWrite: we have all information to initiate (totalBytesExpectedToWrite) and update (totalBytesWritten) progress.

    static void *myContext = &myContext;
    
    - (void)download {
        NSURL *downloadURL = [NSURL URLWithString:@"http://ipv4.download.thinkbroadband.com/10MB.zip"];
        NSURLSession *session =
        [NSURLSession
         sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]
         delegate:self
         delegateQueue:[NSOperationQueue mainQueue]];
    
        [[session downloadTaskWithURL:downloadURL] resume];
    }
    
    #pragma mark - url session download delegate
    
    - (void)URLSession:(NSURLSession *)session
          downloadTask:(NSURLSessionDownloadTask *)downloadTask
          didWriteData:(int64_t)bytesWritten
     totalBytesWritten:(int64_t)totalBytesWritten
    totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite
    {
        if (!_progress) {
            _progress = [NSProgress progressWithTotalUnitCount:totalBytesExpectedToWrite];
            _progress.kind = NSProgressKindFile;
            [_progress addObserver:self forKeyPath:@"fractionCompleted" options:0 context:myContext];
        }
    
        _progress.completedUnitCount = totalBytesWritten;
        //[_overallProgress setUserInfoObject:@1024 forKey:NSProgressEstimatedTimeRemainingKey];
    }
    
    - (void)URLSession:(NSURLSession *)session 
          downloadTask:(NSURLSessionDownloadTask *)downloadTask
    didFinishDownloadingToURL:(NSURL *)location
    {
        [_progress removeObserver:self forKeyPath:@"fractionCompleted"];
        _progress = nil;
        NSLog(@"finished: %@", location);
    }
    
    -(void)observeValueForKeyPath:(NSString *)keyPath
                         ofObject:(id)object
                           change:(NSDictionary *)change
                          context:(void *)context
    {
        if (context == myContext) {
            self.label.text = [_progress localizedAdditionalDescription];
        }
        else {
            [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
        }
    }
    

    In fact, NSProgress doesn't do any calculations. It just formats localized description according to userInfo and other class properties. See Constants section in documentation. You can play with these values and compare localized description outputs.

    If you want to display remaining download time you can use NSProgressEstimatedTimeRemainingKey. Bad news is that you need to calculate remaining time manually. Reference: How to estimate download time remaining (accurately)?

    I tried to set NSProgressThroughputKey which indicates the speed of data processing in bytes per second. I expected that NSProgress would calculate remaining time, but it didn't happen.

    See also AFNetworking sources: AFURLSessionManagerTaskDelegate uses instances of NSProgress for upload and download tasks.