0

After I create a new session data task with URLSession.dataTask(with:completionHandler:) and start the task by calling its resume() method, given that the app doesn't crash while the request is running, is it safe for me to assume that completionHandler (passed to URLSession.dataTask(with:completionHandler:) above) will always eventually get called only once, even if something weird happens with the network request (like if the connection drops) or with the app (like if it goes into the background)?

Note: I'm not explicitly calling cancel() or suspend() on the task. Just resume().

I want to know the answer to this question because (from my app's main thread) I'm creating and starting (one after the other) multiple asynchronous network requests and want to know when the last one has finished.

Specifically, I'm working on an app that has a custom class called Account. On launch, the app (assuming it finds an account access token stored in UserDefaults) creates only one instance of that class and stores it to a global variable (across the entire app) called account, which represents the app's currently-logged-in account.

I've added a stored var (instance) property to Account called pendingGetFooRequestCount (for example) and set it to 0 by default. Every time I make a call to Account.getFoo() (an instance method), I add 1 to pendingGetFooRequestCount (right before calling resume()). Inside completionHandler (passed to URLSession.dataTask(with:completionHandler:) and (to be safe) inside a closure passed to DispatchQueue.main.async(), I first subtract 1 from pendingGetFooRequestCount and then check if pendingGetFooRequestCount is equal to 0. If so, I know the last get-foo request has finished, and I can call another method to continue the flow.

How's my logic? Will this work as expected? Should I be doing this another way? Also, do I even need to decrement pendingGetFooRequestCount on the main thread?

ma11hew28
  • 121,420
  • 116
  • 450
  • 651
  • I would recommend using `OperationQueue` and `Operation` to track and handle multiple concurrent network requests. I would also factor it out into a service instead of having your account deal with it. https://www.hackingwithswift.com/example-code/system/how-to-use-multithreaded-operations-with-operationqueue – Reza Shirazian Feb 11 '19 at 03:39
  • @RezaShirazian, why would you recommend `OperationQueue` & `Operation` over the way described in my question? – ma11hew28 Feb 11 '19 at 13:23
  • are you saying that you need to make multiple requests in series? in other words, don't start the next one until the previous one is finished? – meggar Feb 11 '19 at 14:00
  • @meggar, no, the requests are (from the main thread) created and started (one after the other), but they're executed asynchronously, so I want to know when the last one has finished. Thank you for asking. I've updated my question to make it more clear. – ma11hew28 Feb 11 '19 at 14:36
  • In that case I think you want to use DispatchGroup. https://developer.apple.com/documentation/dispatch/dispatchgroup – meggar Feb 11 '19 at 14:51
  • @meggar, interesting. I didn't know about `DispatchGroup`. That's cool. Thanks for sharing! :-) But, whether I use a stored property (eg, `pendingGetFooRequestCount`) or a `DispatchGroup` to track when all the network requests complete, I still need to know the answer to this question. – ma11hew28 Feb 11 '19 at 15:15
  • Can't use DataTask in background app state, only UploadTask and DowloadTask with callbacks in the AppDelegate instead of completion blocks. Then you would need to set up the URLSessionConfiguration for background too. – meggar Feb 11 '19 at 16:17
  • @meggar, thank you, but that still doesn't answer my question. – ma11hew28 Feb 11 '19 at 16:21
  • You don't need that counter, just use DispatchGroup with a wait(timeout..), it returns a DispatchTimeoutResult - .success means all dataTasks executed the completion block. if you're asking if dataTask's response can call the completion block more than once, it can't. The global account variable is probably not the best option, maybe a shared account would be an improvement `class Account{ static let shared = Account() }`. – meggar Feb 11 '19 at 16:50
  • Why reinvent the wheel. There is already a proven, well-document and well-tested library provided by Foundation that handles what you want to do. Unless there was something overwhelmingly different or extra in your case, I'd recommend `OperationQueue`, `Operation`. – Reza Shirazian Feb 12 '19 at 04:33
  • @meggar, thank you. That's a good idea, but I'm just going to assume the answer to my question is yes—If I'm wrong, please let me know—and just use a counter, as described in my question. Because, using a counter seems simpler to me. Also, I don't see why the global variable isn't good. It seems simpler to me than your suggestion. – ma11hew28 Feb 14 '19 at 23:10
  • @RezaShirazian, thank you, but `URLSession` already has an `OperationQueue` (`delegateQueue`). I don't see how it'd help to create another one. – ma11hew28 Feb 14 '19 at 23:11

1 Answers1

1

URLRequest has a timeoutInterval property, its default value is 60 seconds. If there is no response by then, the completion is called with non-nil error.

meggar
  • 1,219
  • 1
  • 10
  • 18