3

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?

Community
  • 1
  • 1
hgwhittle
  • 9,316
  • 6
  • 48
  • 60

1 Answers1

3

A couple of observations:

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

  2. 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()
        })
    }
    
  3. 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.

  4. 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;
    }];
    
  5. 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).

Rob
  • 415,655
  • 72
  • 787
  • 1,044
  • 1
    Thank you so much for being so thorough. I'll try some of the different things you suggest and post an update. – hgwhittle Feb 10 '16 at 18:59
  • Just out of curiosity, where are you getting the 3 minute time limit for the simpler background task? Apple seems to be pretty vague about the actual time limit, but I'm seeing elsewhere that it can support up to 10 minutes. – hgwhittle Feb 10 '16 at 21:06
  • 2
    It's not formally documented anywhere that I know of, though I have a vague recollection of it being referenced in one of the WWDC videos. It used to be 10 minutes, but is now 3 minutes, AFAIK. If you call [`backgroundTimeRemaining`](https://developer.apple.com/library/ios/documentation/UIKit/Reference/UIApplication_Class/index.html#//apple_ref/occ/instp/UIApplication/backgroundTimeRemaining), it will tell you precisely how much time you have been given. – Rob Feb 10 '16 at 21:12
  • Thanks again. Ended up going with `beginBackgroundTaskWithName`. We might want to use background sessions in the future, but the simpler is working well. – hgwhittle Feb 12 '16 at 16:29
  • @Rob What to do for Uploading task taking more than 3 minutes? I want to upload video to the server with parameters and using AFNetworking. used post, upload task & data task methods. but the system provides only 3 minutes for uploading.after that application terminate and uploading stop. – Bhargav B. Bajani May 08 '17 at 12:53
  • @BhargavB.Bajani - Then you're stuck doing background `URLSession` in which case using AlamoFire becomes much more awkward. It's like http://stackoverflow.com/a/42915383/1271826. Or see http://stackoverflow.com/a/26542755/1271826 (which is download, but goes over some of the issues with background sessions). – Rob May 08 '17 at 14:15
  • it's solved just create backgroundtask and terminate when uploading end. Thanks for our reply – Bhargav B. Bajani May 09 '17 at 09:39