Search code examples
iosswiftafnetworkingafnetworking-3

AFNetworking 3.0 continuing upload when app moves to background


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?


Solution

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