1

Hi all I was just wondering how can I make serial download with NSURLSessionTask in order? what am I looking for is to download the first time once it finished go to the next one but no matter how I try it still goes parallel and not in order. I have tried DISPATCH_QUEUE_SERIAL and dispatch_group_t.

The only way is working is this but the problem is it doesn't call the delegate methods since it calls the completion handler so I can't update the user about the progress. one more thing is I can't use NSURLSessionDownloadTask I have to use "DataTask" .

here is latest code I was trying with no result

-(void)download1{

self.task1 = [ self.session dataTaskWithURL:[NSURL URLWithString:@"https://example.com/file.zip"]];
[self.task1 resume];
}
-(void)download2 {

self.task2 = [self.session dataTaskWithURL:[NSURL URLWithString:@"https://example.com/file.z01"]];

}

-(void)download3 {

self.task3 = [self.session dataTaskWithURL:[NSURL URLWithString:@"https://example.com/file.z02"]];

}

-(void)download:(id)sender {

[self testInternetConnection];

dispatch_queue_t serialQueue = dispatch_queue_create("serial", DISPATCH_QUEUE_SERIAL);
dispatch_sync(serialQueue, ^{
    [self download1];
});

dispatch_sync(serialQueue, ^{
    [self download2];
    [self.task2 resume];
    
});

dispatch_sync(serialQueue, ^{
    [self download3];
    [self.task3 resume];
});



}

Im having only one UIProgressView , and a UILabel to update during the download of each file. Thanks in advance.

The Dreams Wind
  • 8,416
  • 2
  • 19
  • 49
Edi
  • 13
  • 4
  • It seems to you observe `task.progress`, so you should still be able to use the completion if needed. – Larme Sep 09 '22 at 07:40

1 Answers1

0

Per Chunk Progress

You can wrap your operations with NSOperation instances and set up dependencies between them. It's extra-convenient for your scenario, because NSOperationQueue supports NSProgress reporting out of the box. I would still wrap the solution inside of the following interface (a minimalistic example but you can extend it as needed):

@interface TDWSerialDownloader : NSObject

@property(copy, readonly, nonatomic) NSArray<NSURL *> *urls;
@property(strong, readonly, nonatomic) NSProgress *progress;

- (instancetype)initWithURLArray:(NSArray<NSURL *> *)urls;
- (void)resume;

@end

In the anonymous category of the class (implementation file) ensure that you also have a separate property to store NSOperationQueue (it will later needed to retrieve the NSProgress instance):

@interface TDWSerialDownloader()

@property(strong, readonly, nonatomic) NSOperationQueue *tasksQueue;
@property(copy, readwrite, nonatomic) NSArray<NSURL *> *urls;

@end

In the constructor create the queue and make a shallow copy of the urls provided (NSURL doesn't have a mutable counterpart, unlike NSArray):

- (instancetype)initWithURLArray:(NSArray<NSURL *> *)urls {
    if (self = [super init]) {
        _urls = [[NSArray alloc] initWithArray:urls copyItems:NO];
        NSOperationQueue *queue = [NSOperationQueue new];
        queue.name = @"the.dreams.wind.SerialDownloaderQueue";
        queue.maxConcurrentOperationCount = 1;
        _tasksQueue = queue;
    }
    return self;
}

Don't forget to expose the progress property of the queue so views can later get use of it:

- (NSProgress *)progress {
    return _tasksQueue.progress;
}

Now the centrepiece part. You actually don't have control over which thread the NSURLSession performs the requests in, it always happens asynchronously, thus you have to synchronise manually between the delegateQueue of NSURLSession (the queue callbacks are performed in) and the NSOperationQueue inside of operations. I usually use semaphores for that, but of course there is more than one method for such a scenario. Also, if you add operations to the NSOperationQueue, it will try to run them straight away, but you don't want it, as first you need to set up dependencies between them. For this reason you should set suspended property to YES for until all operations are added and dependencies set up. Complete implementation of those ideas are inside of the resume method:

- (void)resume {
    NSURLSession *session = NSURLSession.sharedSession;
    // Prevents queue from starting the download straight away
    _tasksQueue.suspended = YES;
    NSOperation *lastOperation;
    for (NSURL *url in _urls.reverseObjectEnumerator) {
        NSOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
            NSLog(@"%@ started", url);
            __block dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
            NSURLSessionDataTask *task = [session dataTaskWithURL:url completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
                NSLog(@"%@ was downloaded", url);
                // read data here if needed
                dispatch_semaphore_signal(semaphore);
            }];
            [task resume];
            // 4 minutes timeout
            dispatch_semaphore_wait(semaphore, dispatch_time(DISPATCH_TIME_NOW, NSEC_PER_SEC * 60 * 4));
            NSLog(@"%@ finished", url);
        }];
        if (lastOperation) {
            [lastOperation addDependency:operation];
        }
        lastOperation = operation;
        [_tasksQueue addOperation:operation];
    }
    _tasksQueue.progress.totalUnitCount = _tasksQueue.operationCount;
    
    _tasksQueue.suspended = NO;
}

Be advised that no methods/properties of TDWSerialDownloader are thread safe, so ensure you work with it from a single thread.


Here how use of this class looks like in the client code:

TDWSerialDownloader *downloader = [[TDWSerialDownloader alloc] initWithURLArray:@[
    [[NSURL alloc] initWithString:@"https://google.com"],
    [[NSURL alloc] initWithString:@"https://stackoverflow.com/"],
    [[NSURL alloc] initWithString:@"https://developer.apple.com/"]
]];
_mProgressView.observedProgress = downloader.progress;
[downloader resume];

_mProgressView is an instance of UIProgressView class here. You also want to keep a strong reference to the downloader until all operations are finished (otherwise it may have the tasks queue prematurely deallocated).


Per Cent Progress

For the requirements you provided in the comments, i.e. per cent progress tracking when using NSURLSessionDataTask only, you can't rely on the NSOperationQueue on its own (the progress property of the class just tracks number of completed tasks). This is a much more complicated problem, which can be split into three high-level steps:

  1. Requesting length of the entire data from the server;
  2. Setting up NSURLSessionDataDelegate delegate;
  3. Performing the data tasks sequentially and reporting obtained data progress to the UI;

Step 1

This step cannot be done if you don't have control over the server implementation or if it doesn't already support any way to inform the client about the entire data length. How exactly this is done is up to the protocol implementation, but commonly you either use a partial Range or HEAD request. In my example i'll be using the HEAD request:

NSOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
    if (!weakSelf) {
        return;
    }
    
    typeof(weakSelf) __strong strongSelf = weakSelf;
    [strongSelf p_changeProgressSynchronised:^(NSProgress *progress) {
        progress.totalUnitCount = 0;
    }];
    __block dispatch_group_t lengthRequestsGroup = dispatch_group_create();
    for (NSURL *url in strongSelf.urls) {
        dispatch_group_enter(lengthRequestsGroup);
        NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
        request.HTTPMethod = @"HEAD";
        typeof(self) __weak weakSelf = strongSelf;
        NSURLSessionDataTask *task = [strongSelf->_urlSession dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse
*_Nullable response, NSError *_Nullable error) {
            if (!weakSelf) {
                return;
            }
            typeof(weakSelf) __strong strongSelf = weakSelf;
            [strongSelf p_changeProgressSynchronised:^(NSProgress *progress) {
                progress.totalUnitCount += response.expectedContentLength;
                dispatch_group_leave(lengthRequestsGroup);
            }];
        }];
        [task resume];
    }
    dispatch_group_wait(lengthRequestsGroup, DISPATCH_TIME_FOREVER);
}];

As you can see all parts lengths need to be requested as a single NSOperation. The http requests here don't need to be performed in any particular order or even sequently, however the operation still needs to wait until all of them are done, so here is when dispatch_group comes handy.

It's also worth mentioning that NSProgress is quite a complex object and it requires some minor synchronisation to avoid race condition. Also, since this implementation no longer can rely on built-in progress property of NSOperationQueue, we'll have to maintain our own instance of this object. With that in mind here is the property and its access methods implementation:

@property(strong, readonly, nonatomic) NSProgress *progress;

...

- (NSProgress *)progress {
    __block NSProgress *localProgress;
    dispatch_sync(_progressAcessQueue, ^{
        localProgress = _progress;
    });
    return localProgress;
}

- (void)p_changeProgressSynchronised:(void (^)(NSProgress *))progressChangeBlock {
    typeof(self) __weak weakSelf = self;
    dispatch_barrier_async(_progressAcessQueue, ^{
        if (!weakSelf) {
            return;
        }
        typeof(weakSelf) __strong strongSelf = weakSelf;
        progressChangeBlock(strongSelf->_progress);
    });
}

Where _progressAccessQueue is a concurrent dispatch queue:

_progressAcessQueue = dispatch_queue_create("the.dreams.wind.queue.ProgressAcess", DISPATCH_QUEUE_CONCURRENT);

Step 2

Block-oriented API of NSURLSession is convenient but not very flexible. It can only report response when the request is completely finished. In order to get more granular response, we can get use of NSURLSessionDataDelegate protocol methods and set our own class as a delegate to the session instance:

NSURLSessionConfiguration *sessionConfiguration = [NSURLSessionConfiguration defaultSessionConfiguration];
_urlSession = [NSURLSession sessionWithConfiguration:sessionConfiguration
                                            delegate:self
                                       delegateQueue:nil];

In order to listen to the http requests progress inside of the delegate methods, we have to replace block-based methods with corresponding counterparts without them. I also set the timeout to 4 minutes, which is more reasonable for large chunks of data. Last but not least, the semaphore now needs to be used in multiple methods, so it has to turn into a property:

@property(strong, nonatomic) dispatch_semaphore_t taskSemaphore;

...

strongSelf.taskSemaphore = dispatch_semaphore_create(0);
NSURLRequest *request = [[NSURLRequest alloc] initWithURL:url
                                              cachePolicy:NSURLRequestUseProtocolCachePolicy
                                          timeoutInterval:kRequestTimeout];
[[session dataTaskWithRequest:request] resume];

And finally we can implement the delegate methods like this:

- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
    if (error) {
        [self cancel];
        // 3.2 Failed completion
        _callback([_data copy], error);
    }
    dispatch_semaphore_signal(_taskSemaphore);
}

- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data {
    [_data appendData:data];
    [self p_changeProgressSynchronised:^(NSProgress *progress) {
        progress.completedUnitCount += data.length;
    }];
}

URLSession:task:didCompleteWithError: methods additionally checks for error scenarios, but it predominantly should just signal that the current request is finished via the semaphore. Another method accumulates received data and reports current progress.

Step 3

The last step is not really different from what we implemented for Per Chunk Progress implementation, but for sample data I decided to google for some big video-files this time:

typeof(self) __weak weakSelf = self;
TDWSerialDataTaskSequence *dataTaskSequence = [[TDWSerialDataTaskSequence alloc] initWithURLArray:@[
    [[NSURL alloc] initWithString:@"https://download.samplelib.com/mp4/sample-5s.mp4"],
//    [[NSURL alloc] initWithString:@"https://error.url/sample-20s.mp4"], // uncomment to check error scenario
    [[NSURL alloc] initWithString:@"https://download.samplelib.com/mp4/sample-30s.mp4"],
    [[NSURL alloc] initWithString:@"https://download.samplelib.com/mp4/sample-20s.mp4"]
] callback:^(NSData * _Nonnull data, NSError * _Nullable error) {
    dispatch_async(dispatch_get_main_queue(), ^{
        if (!weakSelf) {
            return;
        }
        
        typeof(weakSelf) __strong strongSelf = weakSelf;
        if (error) {
            strongSelf->_dataLabel.text = error.localizedDescription;
        } else {
            strongSelf->_dataLabel.text = [NSString stringWithFormat:@"Data length loaded: %lu", data.length];
        }
    });
}];
_progressView.observedProgress = dataTaskSequence.progress;

With all fancy stuff implemented this sample got a little too big to cover all peculiarities as an SO answer, so feel free to refer to this repo for the reference.

The Dreams Wind
  • 8,416
  • 2
  • 19
  • 49
  • hi thank you for the guide and sorry for the late reply. alright sure ill try to use your guide and see if I can get result. – Edi Sep 11 '22 at 01:21
  • wow thanks its working but the thing here is the progress is waiting for the task to be finished then update the progress. so its working per task and I can't get the percentage like 1% 2% 3% instead of per task. like the way is working on NSURLSession delegate methods. the reason I have to have 3 download in order is my file is big and the iOS device will crash that's why I have to divide the file in to 3 in order for me to download and save them in to document folder and unzip them there . so my first file is 20MB which will take lesser time as compare to the other 2 files with 400MB each. – Edi Sep 11 '22 at 04:15
  • @Edi this is only possible if the server you obtain the data from supports [`Content-Range`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Range_requests) http header, otherwise it's impossible to request the data size for each chunk without downloading it first. – The Dreams Wind Sep 11 '22 at 09:26
  • I believe it is supporting as I was able to get it. cause when I was checking it is giving the Content-Length. – Edi Sep 11 '22 at 09:41
  • @Edi i'll complement my answer later on then, it's quite a big change – The Dreams Wind Sep 11 '22 at 09:42
  • im just so glad for your help thanks a lot I really appreciate. – Edi Sep 11 '22 at 09:58
  • @Edi checkout the updates. The solution is quite verbose, so you may want to consult the minimalistic sample app i pushed to [the repo](https://github.com/AlexandrSMed/SO-a-73661520-5690248-SerialDataTaskSequence) – The Dreams Wind Sep 13 '22 at 22:07
  • thank you so much for your guid I think for my knowledge would have been impossible to do so but there is small problem where I couldnt find solution for that, the download start to begin and showing the progress tho but after a while its giving me this [#1 0x000000010ce377b2 in __52-[ViewController p_subscribeToTaskSequenceProgress:]_block_invoke at /Users/user/Desktop/Cokline/ios 13 up/ios 13 up/ViewController.m:278 ] and [Thread 1: EXC_BAD_ACCESS (code=1, address=0x7885c794f080)] – Edi Sep 14 '22 at 09:10
  • to be exact after receiving of 24MB data it crash. – Edi Sep 14 '22 at 09:32
  • @Edi what device do you use here and how big the file you are trying to load is (24MB doesn't seem overly much for modern iOS devices, however)? Check what exactly you are trying to access at the line 278 of your `ViewController.m` and ensure it doesn't get deallocated before you use it – The Dreams Wind Sep 14 '22 at 10:37
  • im using Xcode sim iPad 9th gen iOS 15 and the file is around 4GB. – Edi Sep 14 '22 at 11:44
  • @Edi the file is huge, but I don't think it's the reason for the error you are experiencing right now (as you said the loading only had 24 mb before the app crashes), - first ensure that you don't work with zombie objects at line 278 (i just don't know what exactly is there, if you can share the code we can analyse it further). Second - loading a 4GB file in memory doesn't seem reasonable, that can easily cause memory overflow. Why can't you use download tasks? If the file is video/music, why not pass it to an `AVPlayer` directly? Otherwise, you might want to store it right on the disk – The Dreams Wind Sep 14 '22 at 12:06
  • sure I will share the code if you don't mind I'm outside my home ill share with once I'm back, actually you are correct 4GB is big and for some reasons Download Task Delegates is not working for me, I'm able to download the file but I'm not able to get progress and save it to document folder I've tried different ways that's the only way I could get progress of my download, and lastly to avoid the app crashes for such a big file I divid in to multiple files and that's why i was trying to get serial download which your knowledge and comments saved me thanks for that. – Edi Sep 14 '22 at 12:23
  • the problem of NSURLDownloadTaskDelegate is the last 2 methods are not getting called as it supposed. that's why I can't get the progress and can't save the file. – Edi Sep 14 '22 at 12:28
  • @Edi if you don't need such a big file in-memory (and you probably shouldn't as it's too much for an iOS application) the most proper solution here is to make one single URL to download the file, and ensure the server supports the `Range` http header (so it's possible to do partial downloads). `NSURLDownloadSession` can load it directly to the filesystem, offering much more than that out of the box (like background download, progress tracking with the delegate methods, pausing and resuming the download operation).. – The Dreams Wind Sep 14 '22 at 16:22
  • @Edi ..In case you can't do that for whatever reason (i'm not really following why exactly), you can replace in-memory storage with on-disk storage in my implementation. I made some amendments in the repo i linked earlier to show how it can be done, but that's not really a proper way of loading/working with big files from remote. Hope it helps – The Dreams Wind Sep 14 '22 at 16:25
  • yes you are correct and I'm aware of that as well but again the only problem is the delegate methods are not called during the process, I don't know why it doesn't call. that's why I decided to change it to DataTask instead. here is the code [link](https://pastebin.com/EzZE5hvM). – Edi Sep 16 '22 at 03:12
  • @Edi line 278 doesn't refer to anything suspicious and tbh the error text somewhat contradicts the source code you provided (it says the access problem happens inside of a block in `p_subscribeToTaskSequenceProgress` method, while in the source code provided line 278 is inside another construction (`p_unsubscribeFromTaskSequenceProgress` method). I assume the code is somewhat changed (some comments section removed or other lines of code shifted to hide sensitive data), but my bet is that `_label` (i.e. `self.label`) property nullified for some reason while the code expects it to be alive – The Dreams Wind Sep 16 '22 at 06:29
  • oh alright thanks for your help man I really appreciate it ill gonna try to fix it on Monday. – Edi Sep 16 '22 at 09:12