5

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.

Frederik
  • 436
  • 5
  • 22
  • Implement [`didCompleteWithError`](https://developer.apple.com/reference/foundation/urlsessiontaskdelegate/1411610-urlsession). And if you want progress updates as it goes, you can use [`didSendBodyData`](https://developer.apple.com/reference/foundation/urlsessiontaskdelegate/1408299-urlsession). – Rob Nov 17 '16 at 14:24
  • 1
    Unrelated, but rather than instantiating `NSURL` and `NSMutableURLRequest` and casting to `URL` and `URLRequest`, you should just create the `URL` and `URLRequest` in the first place. The only trick is that since you want to mutate the `URLRequest`, use `var`, not `let`. – Rob Nov 17 '16 at 14:26
  • @Rob I have done that but my problem is how to "get out of" the `CustomDelegate` class. If I'm for example in `mainViewController` and I call a function which does the setup of the `uploadTask` then I want to update the UI for example – Frederik Nov 17 '16 at 14:27
  • You give your custom delegate class some mechanism to inform the main view controller. The typical ways are a "completion handler" closure or protocol-delegate pattern (i.e. 1. have your custom delegate object define a protocol by which it will inform about updates, 2. have its own `delegate` property of that type, 3. have the main view controller conform to that protocol, and 4. when you start the request, set the view controller as the delegate of the "custom delegate object"). Or post a notification. – Rob Nov 17 '16 at 14:32
  • Thanks for the answer @Rob. Is it possible to use a completion handler closure with background upload tasks? – Frederik Nov 18 '16 at 07:19
  • No. As [the documentation](https://developer.apple.com/reference/foundation/urlsession) says, "For all background downloads and uploads, you must provide a `delegate` that conforms to the `URLSessionDownloadDelegate` protocol." – Rob Nov 18 '16 at 07:23
  • Ah ok, I'm still a little confused, could you show an example in code how you would for example update the UI from within the `CustomDelegate` class? Much appreciated! – Frederik Nov 18 '16 at 07:59

1 Answers1

3

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).

Rob
  • 371,891
  • 67
  • 713
  • 902
  • thanks a ton, great answer! I have gotten it working except for getting the status code. I think that I'm implementing `didReceiveResponse` incorrectly since it is never called no matter what. Got any ideas as of why? Cheers. – Frederik Nov 23 '16 at 13:11
  • I'd double check the method signature and make sure that's right: https://developer.apple.com/reference/foundation/urlsessiondatadelegate/1410027-urlsession. Otherwise, I'd suggest posting a new question with a [reproducible example of the problem](http://stackoverflow.com/help/mcve). – Rob Nov 23 '16 at 16:49
  • @Rob I'm trying to implement a delegate for a download task with a background session but my didFinishDownloadingTo is not being triggered. If you get a chance, I would appreciate if you could take a look at: http://stackoverflow.com/questions/41488989/using-urlsession-and-background-fetch-together-with-remote-notifications-using-f?noredirect=1#comment70190510_41488989 – user2363025 Jan 06 '17 at 15:38