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