10

URLSession data task block is not calling when the app is in background and it stuck at dataTask with request.
When I open the app the block gets called. By the way I'm using https request.
This is my code:

    let request = NSMutableURLRequest(url: URL(string: url as String)!,

                                      cachePolicy: .reloadIgnoringCacheData,

                                      timeoutInterval:20)

    request.httpMethod = method as String

    request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")

    let session = URLSession.shared

    let data = params.data(using: String.Encoding.utf8.rawValue)

    request.httpBody = data

    session.dataTask(with: request as URLRequest,completionHandler:

        {(data, response, error) -> Void in

         if error == nil

            {

                do {

                    let result = try JSONSerialization.jsonObject(with: data!, options:

                        JSONSerialization.ReadingOptions.mutableContainers)

                    print(result)

                     completionHandler(result as AnyObject?,nil)

                }

                catch let JSONError as NSError{

                    completionHandler(nil,JSONError.localizedDescription as NSString?)

                }

            }

            else{

                completionHandler(nil,error!.localizedDescription as NSString?)                    

            }                

    }).resume()

Working perfectly when the app is in active state. Is there anything wrong in my code. please point me

D4ttatraya
  • 3,344
  • 1
  • 28
  • 50
Test Test
  • 1,761
  • 3
  • 10
  • 12
  • Not related, but why do you pass `.mutableContainers` but assign the result to an immutable constant? And don't use `NSURLRequest` and `NSString` in Swift, use the native structs. Finally `String.Encoding.utf8.rawValue` can be just written `.utf8`, this is much less code to type (or to paste). – vadian May 23 '17 at 08:11
  • 1
    Is `session` a background session? If so, do a delegate-based download task, not a data task. See [Background Transfer Considerations](https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/URLLoadingSystem/Articles/UsingNSURLSession.html#//apple_ref/doc/uid/TP40013509-SW44). – Rob May 23 '17 at 08:24
  • that means i can use download task eventhough there nothing to download. – Test Test May 23 '17 at 08:26
  • 1
    Download the JSON response, when when you implement [`handleEventsForBackgroundURLSession`](https://developer.apple.com/reference/uikit/uiapplicationdelegate/1622941-application), load the contents and parse it. – Rob May 23 '17 at 08:27
  • @Rob can ugive me a link or sample code to it. I'm newbie – Test Test May 23 '17 at 09:34

4 Answers4

39

If you want downloads to progress after your app is no longer in foreground, you have to use background session. The basic constraints of background sessions are outlined in Downloading Files in Background, and are essentially:

  1. Use delegate-based URLSession with background URLSessionConfiguration.

  2. Use upload and download tasks only, with no completion handlers.

  3. In iOS, Implement application(_:handleEventsForBackgroundURLSession:completionHandler:) app delegate, saving the completion handler and starting your background session.

    Implement urlSessionDidFinishEvents(forBackgroundURLSession:) in your URLSessionDelegate, calling that saved completion handler to let OS know you're done processing the background request completion.

So, pulling that together:

func startRequest(for urlString: String, method: String, parameters: String) {
    let url = URL(string: urlString)!
    var request = URLRequest(url: url, cachePolicy: .reloadIgnoringCacheData, timeoutInterval: 20)
    request.httpMethod = method
    request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
    request.httpBody = parameters.data(using: .utf8)
    BackgroundSession.shared.start(request)
}

Where

class BackgroundSession: NSObject {
    static let shared = BackgroundSession()
    
    static let identifier = "com.domain.app.bg"
    
    private var session: URLSession!

    #if !os(macOS)
    var savedCompletionHandler: (() -> Void)?
    #endif
    
    private override init() {
        super.init()
        
        let configuration = URLSessionConfiguration.background(withIdentifier: BackgroundSession.identifier)
        session = URLSession(configuration: configuration, delegate: self, delegateQueue: nil)
    }
    
    func start(_ request: URLRequest) {
        session.downloadTask(with: request).resume()
    }
}

extension BackgroundSession: URLSessionDelegate {
    #if !os(macOS)
    func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) {
        DispatchQueue.main.async {
            self.savedCompletionHandler?()
            self.savedCompletionHandler = nil
        }
    }
    #endif
}

extension BackgroundSession: URLSessionTaskDelegate {
    func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
        if let error = error {
            // handle failure here
            print("\(error.localizedDescription)")
        }
    }
}

extension BackgroundSession: URLSessionDownloadDelegate {
    func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
        do {
            let data = try Data(contentsOf: location)
            let json = try JSONSerialization.jsonObject(with: data)
            
            print("\(json)")
            // do something with json
        } catch {
            print("\(error.localizedDescription)")
        }
    }
}

And the iOS app delegate does:

func application(_ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: @escaping () -> Void) {
    BackgroundSession.shared.savedCompletionHandler = completionHandler
}
Community
  • 1
  • 1
Rob
  • 415,655
  • 72
  • 787
  • 1,044
  • 2
    I just tested it, working perfectly. million thanks bro – Test Test May 23 '17 at 17:54
  • Okay, this is awesome. Thanks so much, @Rob. But how can I access / receive a completion handler in the startRequest function? If I want to execute this same dataTask (downloadTask) both from a potential background fetch or from a ViewController? e.g. If I use a UIRefreshControl() on a UITableViewController to execute the same API call, how would I stop it from spinning when the fetch is "done"? – David Vincent Gagne Oct 20 '18 at 04:57
  • 1
    There are many ways to tackle this. The bottom line, though, is that you want to do something consistent with the delegate pattern of background sessions. For example, you can have your session’s task delegate method post a notification when it’s done and then have your view controller observe that notification and do whatever it needs there. But you need a pattern which will work even if the downloads were in progress when app was jettisoned during its normal lifecycle and later restarted. – Rob Oct 20 '18 at 16:11
  • Thanks for the reply, @Rob. Where you have "do something with json", is it okay if that is in a `DispatchQueue.main.async`? I ask because mine is -- because it needs to update the application badge -- and it seems to hang there until / unless I return the app to the foreground. If I remove the DispatchQueue wrapper, it doesn't hang but then it crashes because it can't update the UI from a background thread ... – David Vincent Gagne Oct 24 '18 at 17:43
  • 2
    I also am confused because this article https://developer.apple.com/documentation/foundation/url_loading_system/downloading_files_in_the_background clearly says: "Not all background network activity has to be done with background sessions as described in this article. Apps that declare appropriate background modes can use default URL sessions and data tasks, just as if they were in the foreground." – David Vincent Gagne Oct 24 '18 at 21:02
  • Can you use `DispatchQueue.main.async` from within the `URLSession` delegate methods? Absolutely. "Not all background network activity has to be done with background sessions..." That is correct. If your app is employing some other mechanism that happens to have the app running in the background, the background `URLSession` is not needed. The point of background `URLSession` is to hand off network requests to a network daemon while your app is suspended and, potentially in the normal course of events, terminated. But if your app is already running in the background, then none of this needed. – Rob Oct 29 '18 at 15:00
  • @Rob I am not able to override urlSessionDidFinishEvents for macos? – The iCoder Aug 08 '19 at 15:53
  • @Rob This is great! I'm trying to use this in my WatchKit project, all seems to be well but I'm having trouble picking up the `handleEventsForBackgroundURLSession` How can I get this to be handled in my `InterfaceController`? – Jordan Aug 08 '19 at 21:00
  • 1
    @TheiCoder - That feature is not available in macOS. In iOS, if the background session wakes your app, it passes a completion handler to your app delegate, and it provides this `urlSessionDidFinishEvents` so that your app knows that it can call the saved completion handler, letting iOS know that you’re done and that the app can be suspended again. But macOS doesn’t do this, so this method is not needed. – Rob Aug 08 '19 at 21:35
  • @Rob I've put all this code at the top of my controller, is it supposed to go somewhere else? – Jordan Aug 09 '19 at 15:40
  • @Rob I made a new question with my actual code, would love if you could look at it and see if I'm implementing it wrong https://stackoverflow.com/questions/57433884/cant-get-watchkit-urlsession-background-to-work – Jordan Aug 09 '19 at 16:19
  • Does a notification service extension constitute a need to use `downloadTask` instead of `dataTask`? I'm seeing some random problems with my notification service extension and wonder if this is the cause – mfaani Jun 09 '20 at 10:50
  • @Honey - The only time you need download tasks is with background `URLSessionConfiguration`. The other issue is that, with large assets, download tasks require far less memory, so maybe that’s a consideration. It’s impossible to say on the basis of the info you’re provided. I’d suggest posting your own question with a MCVE. – Rob Jun 09 '20 at 16:03
  • how can we test it in simulator? – LiangWang Apr 17 '23 at 03:20
  • The most important thing to test is to *not* be attached to the Xcode debugger (because that artificially keeps the app running in the background, which defeats the whole purpose of background sessions). So, at that point, why bother with simulator at all?! – Rob Apr 17 '23 at 04:58
1

Or simply start a BackgroundTask

func send(...) {
  let backgroundTaskID = UIApplication.shared.beginBackgroundTask(expirationHandler: nil)
  let session = URLSession(configuration: .default)

  // ... after all the main logic, when you finish:
  DispatchQueue.main.async {
    completion(result)
    UIApplication.shared.endBackgroundTask(backgroundTaskID)
  }
}
raed
  • 4,887
  • 4
  • 30
  • 49
0

You need a background session. The URLSessionDataTask which as per Apple's documentation doesn't support background downloads.

Create a URLSessionDownloadTask and use its delegate method it should work.

Follow this link

rameez
  • 374
  • 2
  • 11
-1
  [URLSessionDownloadTask setDownloadTaskDidWriteDataBlock:^(NSURLSession *session, NSURLSessionDownloadTask *downloadTask, int64_t bytesWritten, int64_t totalBytesWritten, int64_t totalBytesExpectedToWrite) {
                CGFloat percentDone = (double)(totalBytesWritten)/(double)totalBytesExpectedToWrite;
                [SVProgressHUD showWithStatus:[NSString stringWithFormat:@"%.2f%%",percentDone*100]];

            }];

        [downloadTask resume];

// Apply as shown in picture

/*********************/enter image description here

Ajjjjjjjj
  • 669
  • 4
  • 12