4

I know I can use dataTaskWithURL:completionHandler: to get the data in the completionHandler block, but that blocks the delegate methods from firing, and I need the didReceiveData: method to fire, as it's how I configure my progress indicator.

I'm completely at a loss how to get the downloaded data once it's complete. What's the delegate method equivalent of the completion block? didCompleteWithError doesn't seem to return any NSData.

I don't have to manually piece the data together in didReceiveData, do I? That seems really lame when the completionHandler just hands it off to you. I wouldn't mind doing that if it weren't for the fact that I could be downloading 50+ things at once, so keeping track of all that partial data seems like a pain in the ass. Should I just switch to NSURLSessionDownloadTask?

  • I have written a downloader class (using a download task) that might help you: https://github.com/mattneub/Programming-iOS-Book-Examples/blob/master/bk2ch24p842downloader/ch37p1099downloader/MyDownloader.m – matt Feb 21 '14 at 04:28
  • So this is then an implementation of what [Rob is referring to](http://stackoverflow.com/a/21925050/2005643) in his last paragraph? –  Feb 21 '14 at 04:34
  • Did you look at it? Did you look at what I said in my previous comment? – matt Feb 21 '14 at 04:37
  • @Aloha64 It would appear that Matt's solution is based upon `NSURLSessionDownloadTask`. And if you're downloading 50+ files and simply want to use the delegate methods to track the progress, using a download task is probably simplest. It handles memory efficiently, keeps track of the separate downloads, and gives you `didWriteData` method so you can update your progress indicator views without dragging you through the weeds of maintaining your own `NSMutableData` or `NSOutputStream` objects for each download. – Rob Feb 21 '14 at 05:49

1 Answers1

6

Yes, you have to manually piece the data together (or you can stream it to a file if it's really big and you don't want it taking up memory).

So, didReceiveData method will be returning your data as it comes in. So you should have instantiated a NSMutableData (for example, in didReceiveResponse) to which didReceiveData will append the data as it comes in. When didCompleteWithError is called, assuming the error is nil, you can be confident that your NSMutableData now contains all of the data received. As you noted, the challenge is keeping track of all of the 50+ downloads, so I maintain an dictionary keyed by task identifiers to keep track of which to append the data to. (Personally, I think it's a design flaw that NSURLSession implements the task, download, and upload delegates at the session level, rather than letting us instantiate separate task delegate objects for each task. But we're stuck with what we've got.)

If you're just downloading the data, the NSURLSessionDownloadTask is a great alternative (and is more efficient in terms of memory usage than just appending to NSMutableData instances), and you can conceivably also use a background session if you want (which you can't with a NSURLSessionDataTask).

Finally, if you're really doing 50+ downloads, you might want to consider wrapping the download tasks in NSOperation subclass so you can constrain how many run concurrently without risking having any timeout.

Rob
  • 415,655
  • 72
  • 787
  • 1,044
  • By the way, the authors of AFNetworking are reporting (see [issue 1504](https://github.com/AFNetworking/AFNetworking/issues/1504)) that a forthcoming release will be wrapping `NSURLSessionTask` objects in `NSOperation` subclass, so if you don't want to write your own class to do that (like some of us have done), you might keep an eye out for AFNetworking updates. – Rob Feb 21 '14 at 04:17
  • Hmm, in reality I doubt I'd ever be doing more than 10. Is it worth wrapping it in NSOperations? I guess I should probably just test and see. –  Feb 21 '14 at 04:32
  • Extremely educational answer. - I do worry, though, because the documentation seems to suggest that with a data task you cannot rely on the pieces of the data to arrive in order - in which case merely appending to a mutable data each time would not work. – matt Feb 21 '14 at 04:33
  • @Aloha64 If you test this, make sure you use the network link conditioner to test in worst case scenario, because you might not suffer from timeout-related problems that would only manifest themselves on incredibly slow connections. Personally, if doing more than four concurrent requests, I'd always use `NSOperation`-based approach to avoid the problem altogether. Or bump your timeout to a really large value (which you do at the `NSURLSessionConfiguration` object). – Rob Feb 21 '14 at 04:44
  • @matt Certainly you cannot assume that the `didReceiveData` calls will be called sequentially across multiple tasks (and for concurrent requests, I would not expect/want them to). But for each respective task, they must come in the correct order, or else it would render `didReceiveData` largely useless. Thus, if you maintain separate `NSMutableData` objects for each task, you should be fine. If you're really saying that for a given task, the `didReceiveData` calls might not come in order, perhaps you can share that reference to that, as I've never read nor experienced anything of that nature. – Rob Feb 21 '14 at 05:01
  • It's the part where they say "Because the NSData object is often pieced together from a number of different data objects, whenever possible, use NSData’s enumerateByteRangesUsingBlock: method to iterate through the data rather than using the bytes method (which flattens the NSData object into a single memory block)." (https://developer.apple.com/library/ios/documentation/Foundation/Reference/NSURLSessionDataDelegate_protocol/Reference/Reference.html#//apple_ref/occ/intfm/NSURLSessionDataDelegate/URLSession:dataTask:didReceiveData:) – matt Feb 21 '14 at 05:04
  • Back in the NSURLConnection days, I always just appended to a mutable data. But that line in the NSURLSession documentation fills me with apprehension... – matt Feb 21 '14 at 05:05
  • 1
    @matt Ah, ok. That `enumerateByteRangesUsingBlock` reference is not suggesting that the requests are not coming in order, but rather that the resulting `NSMutableData` may not be storing the byte ranges in a single, contiguous byte range (which is absolutely true). It's just a warning that someone should not blithely use `getBytes` to get the full `NSData` contents (because if source was not contiguous, it's going to copy it to a contiguous range before getting the bytes, which can be inefficient). It's an interesting observation about `NSData`, but, IMHO, not relevant to the question at hand. – Rob Feb 21 '14 at 05:17
  • 1
    Thanks, glad to hear it. It scared me off using data tasks! It _looked_ like I was always getting the right answer, but I was afraid that this was just luck, and that some terrible problem was waiting to happen... – matt Feb 21 '14 at 05:21