My app initiates uploading content to a server while in the foreground. Since this process could take some time, the user could very well transition the app to the background.
I'm using AFHTTPSessionManager to submit the upload:
let sessionManager = AFHTTPSessionManager()
sessionManager.requestSerializer.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-type")
sessionManager.POST(urlStr, parameters: params, constructingBodyWithBlock: { (formData) -> Void in
formData.appendPartWithFileData(dataObject, name: "object", fileName: mimeType == "image/jpg" ? "pic.jpg" : "pic.mp4", mimeType: mimeType)
}, progress: { (progress) -> Void in
}, success: { (task, responseObject) -> Void in
print(responseObject)
success(responseObject: responseObject)
}, failure: { (task, error) -> Void in
print(error)
if let errorData = error.userInfo[AFNetworkingOperationFailingURLResponseDataErrorKey] as? NSData {
do {
let json = try NSJSONSerialization.JSONObjectWithData(errorData, options: NSJSONReadingOptions.MutableContainers)
failure(error: error, responseObject: json)
} catch let error {
print(error)
}
}
})
I need this process to continue while the app is in the background state until completion. There's an excellent SO answer here which has gotten me on the right track, but I'm still in the dark in some places. I've tried using the "BackgroundSessionManager" class from the linked answer to change my upload call to this:
BackgroundSessionManager.sharedManager().POST(NetworkManager.kBaseURLString + "fulfill_request", parameters: params, constructingBodyWithBlock: { (formData) -> Void in
formData.appendPartWithFileData(dataObject, name: "object", fileName: mimeType == "image/jpg" ? "pic.jpg" : "pic.mp4", mimeType: mimeType)
}, progress: nil, success: nil, failure: nil)?.resume()
And added this to my AppDelegate:
func application(application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: () -> Void) {
BackgroundSessionManager.sharedManager().savedCompletionHandler = completionHandler
}
But I'm getting an EXC_BAD_ACCESS crash on some background thread with little to no information. Am I even approaching this correctly?
A couple of observations:
You are building a multipart request, but then forcing the Content-Type
header to be application/x-www-form-urlencoded
. But that's not a valid content header for multipart requests. I'd suggest removing that manual configuration of the request header (or tell us why you're doing that). AFNetworking will set the Content-Type
for you.
Rather than bothering with background sessions at all, you might just want to request a little time to complete the request even after the user leaves the app.
var backgroundTask = UIBackgroundTaskInvalid
func uploadImageWithData(dataObject: NSData, mimeType: String, urlStr: String, params: [String: String]?, success: (responseObject: AnyObject?) -> (), failure: (error: NSError, responseObject: AnyObject) -> ()) {
let app = UIApplication.sharedApplication()
let endBackgroundTask = {
if self.backgroundTask != UIBackgroundTaskInvalid {
app.endBackgroundTask(self.backgroundTask)
self.backgroundTask = UIBackgroundTaskInvalid
}
}
backgroundTask = app.beginBackgroundTaskWithName("com.domain.app.imageupload") {
// if you need to do any addition cleanup because request didn't finish in time, do that here
// then end the background task (so iOS doesn't summarily terminate your app
endBackgroundTask()
}
let sessionManager = AFHTTPSessionManager()
sessionManager.POST(urlStr, parameters: params, constructingBodyWithBlock: { (formData) -> Void in
formData.appendPartWithFileData(dataObject, name: "object", fileName: mimeType == "image/jpg" ? "pic.jpg" : "pic.mp4", mimeType: mimeType)
}, progress: { (progress) -> Void in
}, success: { (task, responseObject) -> Void in
print(responseObject)
success(responseObject: responseObject)
endBackgroundTask()
}, failure: { (task, error) -> Void in
print(error)
if let errorData = error.userInfo[AFNetworkingOperationFailingURLResponseDataErrorKey] as? NSData {
do {
let json = try NSJSONSerialization.JSONObjectWithData(errorData, options: NSJSONReadingOptions.MutableContainers)
failure(error: error, responseObject: json)
} catch let error {
print(error)
}
}
endBackgroundTask()
})
}
I'd suggest adding an exception breakpoint and see if that helps you track down the offending line.
Frankly, if the exception is occurring asynchronously inside the NSURLSession
internal processing, this might not help, but it's the first thing I try whenever trying to track down the source of an exception.
If you really feel like you must use background session (i.e. your upload task(s) are likely to take more than the three minutes that beginBackgroundTaskWithName
permits), then be aware that background task must be upload or download tasks.
But, I believe that the POST
method will create a data task. In iOS 7, you aren't allowed to use data tasks with background sessions at all. In iOS 8, you can initiate a data task with a background task, but you have to respond to didReceiveResponse
with NSURLSessionResponseBecomeDownload
, e.g. in configureDownloadFinished
of that BackgroundSessionManager
, you can try adding:
[self setDataTaskDidReceiveResponseBlock:^NSURLSessionResponseDisposition(NSURLSession *session, NSURLSessionDataTask *dataTask, NSURLResponse *response) {
return NSURLSessionResponseBecomeDownload;
}];
Alternatively, if, again, you must use background session, then you might want manually build your NSURLRequest
object and just submit it as an upload or download task rather than using POST
. AFNetworking provides mechanisms to decouple the building of the request and the submitting of the request, so if you need to go down that road, let us know and we can show you how to do that.
Bottom line, beginBackgroundTaskWithName
is going to be much easier way to request a little time to finish a request than using background NSURLSession
. Only do the latter if you absolutely must (e.g. there's a good chance that you'll need more than three minutes to finish the request).