swiftnsurlsessiondatatasknsurlsessionuploadtask

Implementing completion handlers for backgroundSession.uploadTask


I have (almost) successfully implemented URLSessionDelegate, URLSessionTaskDelegate, and URLSessionDataDelegate so that I can upload my objects in the background. But I'm not sure how to implement completion handlers, so that I can delete the object that I sent, when the server returns statuscode=200

I currently start the uploadTask like this

let configuration = URLSessionConfiguration.background(withIdentifier: "com.example.myObject\(myObject.id)")
let backgroundSession = URLSession(configuration: configuration, 
                                   delegate: CustomDelegate.sharedInstance, 
                                   delegateQueue: nil)
let url: NSURL = NSURL(string: "https://www.myurl.com")!
let urlRequest = NSMutableURLRequest(url: url as URL)

urlRequest.httpMethod = "POST"
urlRequest.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")

let uploadTask = backgroundSession.uploadTask(with: urlRequest as URLRequest, fromFile: path)

uploadTask.resume()

I tried adding a closure to the initialization of uploadTask but xcode displayed an error that it was not possible.

I have my custom class CustomDelegate:

class CustomDelegate : NSObject, URLSessionDelegate, URLSessionTaskDelegate, URLSessionDataDelegate {

static var sharedInstance = CustomDelegate()

func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
    print("\(session.configuration.identifier!) received data: \(data)")
    do {
        let parsedData = try JSONSerialization.jsonObject(with: data, options: .allowFragments) as! [String:Any]
        let status = parsedData["status"] as! NSDictionary
        let statusCode = status["httpCode"] as! Int

        switch statusCode {
        case 200:
            // Do something
        case 400:
            // Do something
        case 401:
            // Do something
        case 403:
            // Do something
        default:
            // Do something
        }
    }
    catch {
        print("Error parsing response")
    }
}
}

It also implements the other functions for the delegates.

What I want is to somehow know that the upload is done so that I can update the UI and database which I feel is hard (maybe impossible?) from within CustomDelegate.


Solution

  • If you're only interested in detecting the completion of the request, the simplest approach is to use a closure:

    class CustomDelegate : NSObject, URLSessionDelegate, URLSessionTaskDelegate, URLSessionDataDelegate {
    
        static var sharedInstance = CustomDelegate()
        var uploadDidFinish: ((URLSessionTask, Error?) -> Void)?
    
        func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
            DispatchQueue.main.async {
                uploadDidFinish?(task, error)
            }
        }
    
    }
    

    Then your view controller would set this closure before initiating the request, e.g.

    CustomDelegate.sharedInstance.uploadDidFinish = { [weak self] task, error in
        // update the UI for the completion here
    }
    
    // start the request here
    

    If you want to update your UI for multiple situations (e.g. not only as uploads finish, but progress as the uploads are sent), you theoretically could set multiple closures (one for completion, one for progress), but often you'd adopt your own delegate-protocol pattern. (Personally, I'd rename CustomDelegate to something like UploadManager to avoid confusion about who's a delegate to what, but that's up to you.)

    For example you might do:

    protocol UploadDelegate: class {
        func didComplete(session: URLSession, task: URLSessionTask, error: Error?)
        func didSendBodyData(session: URLSession, task: URLSessionTask, bytesSent: Int64, totalBytesSent: Int64, totalBytesExpectedToSend: Int64)
    }
    

    Then, in your network request manager (your CustomDelegate implementation), define a delegate property:

    weak var delegate: UploadDelegate?
    

    In the appropriate URLSession delegate methods, you'd call your custom delegate methods to pass along the information to the view controller:

    func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
        // do whatever you want here
    
        DispatchQueue.main.async {
            delegate?.didComplete(session: session, task: task, didCompleteWithError: error)
        }
    }
    
    func urlSession(_ session: URLSession, task: URLSessionTask, didSendBodyData bytesSent: Int64, totalBytesSent: Int64, totalBytesExpectedToSend: Int64) {
        // do whatever you want here
    
        DispatchQueue.main.async {
            delegate?.didSendBodyData(session: session, task: task, bytesSent: bytesSent, totalBytesSent: totalBytesSent, totalBytesExpectedToSend: totalBytesExpectedToSend)
        }
    }
    

    Then, you'd declare your view controller to conform to your new protocol and implement these methods:

    class ViewController: UIViewController, UploadDelegate {
        ...
        func startRequests() {
            CustomDelegate.sharedInstance.delegate = self
    
            // initiate request(s)
        }
    
        func didComplete(session: URLSession, task: URLSessionTask, error: Error?) {
            // update UI here
        }
    
        func didSendBodyData(session: URLSession, task: URLSessionTask, bytesSent: Int64, totalBytesSent: Int64, totalBytesExpectedToSend: Int64) { 
            // update UI here
        }
    }
    

    Now, you might update this UploadDelegate protocol to capture model information and pass that as a parameter to your methods, too, but hopefully this illustrates the basic idea.


    Some minor observations:

    1. When creating your session, you probably should excise the NSURL and NSMutableURLRequest types from your code, e.g.:

      let url = URL(string: "https://www.myurl.com")!
      var urlRequest = URLRequest(url: url)
      
      urlRequest.httpMethod = "POST"
      urlRequest.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")
      
      let uploadTask = backgroundSession.uploadTask(with: urlRequest, fromFile: path)
      
      uploadTask.resume()
      
    2. You are looking for statusCode in didReceiveData. You really should be doing that in didReceiveResponse. Also, you generally get the status code from the URLResponse.

    3. You are parsing the response in didReceiveData. Generally, you should do that in didCompleteWithError (just in case it takes multiple calls to didReceiveData to receive the entire response).

    4. I don't know what this myObject.id is, but the identifier you've chosen, "com.example.myObject\(myObject.id)", is somewhat suspect:

      • Are you creating a new URLSession instance for each object? You probably want one for all of the requests.

      • When your app is suspended/jettisoned while the upload continues in the background, when the app is restarted, do you have a reliable way of reinstantiating the same session objects?
         

      Generally you'd want a single upload session for all of your uploads, and the name should be consistent. I'm not saying you can't do it the way you have, but it seems like it's going to be problematic recreating those sessions without going through some extra work. It's up to you.

      All of this is to say that I'd make sure you test your background uploading process works if the app is terminated and is restarted in background when the uploads finish. This feels like this is incomplete/fragile, but hopefully I'm just jumping to some incorrect conclusions and you've got this all working and simply didn't sharing some details (e.g. your app delegate's handleEventsForBackgroundURLSession) for the sake of brevity (which is much appreciated).