4

I need to upload content to AWS S3 via PUT that can be run in a background session using NSURLSessionUploadTask.

So far it works great. However I need to then call my API once the upload to S3 has finished to change it's state to complete.

I AWSS3 to create the S3 request and then I copy it to an NSURLSessionUploadTask as per this SO answer. This runs in both the foreground and the background, and uploads the file ok to S3.

Now this is the part I need help with. I've tried to use both URLSession:task:didCompleteWithError and URLSessionDidFinishEventsForBackgroundURLSession delegate methods to call my additional API request, I've used both standard data tasks as well as download tasks they don't seem to fire the request in the background, until I open the app again. Ideally I'd like them to fire straight away. They do fire of the upload is in the foreground. I've seen Wunderlist do exactly what I'm trying to do in the background, not sure how they do it.

Here's what I have so far ... Any help/suggestions would be amazing, this is driving me crazy! :)

- (IBAction)upload:(id)sender {
  AmazonS3Client *s3Client = [[AmazonS3Client alloc] initWithAccessKey:@"ABC" withSecretKey:@"123"];

  NSString *identifier = [NSString stringWithFormat:@"com.journeyhq.backgroundSession.%@-%@", @"S3", @"ATTACHMENT_REMOTE_ID"];
  NSURLSessionConfiguration *sessionConfiguration = [NSURLSessionConfiguration backgroundSessionConfiguration:identifier];
  NSURLSession *session = [NSURLSession sessionWithConfiguration:sessionConfiguration delegate:self delegateQueue:nil];

  NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
  NSString *documentsDirectory = [paths objectAtIndex:0];
  NSString *filePath = [documentsDirectory stringByAppendingPathComponent:@"img.jpg"];
  NSURL *fromFile = [NSURL fileURLWithPath:filePath isDirectory:NO];

  S3PutObjectRequest *s3Request = [[S3PutObjectRequest alloc] initWithKey:[[NSString stringWithFormat:@"%@__%@__%@", @"ATTACHMENT_ID", @"ATTACHMENT_UUID", @"img.jpg"] lowercaseString] inBucket:@"BUCKET_NAME"];
  s3Request.cannedACL = [S3CannedACL publicReadWrite];

  NSMutableURLRequest *request = [s3Client signS3Request:s3Request];

  NSString *urlString = [NSString stringWithFormat:@"https://%@.%@/%@", @"BUCKET_NAME", @"s3-eu-west-1.amazonaws.com", [[NSString stringWithFormat:@"%@__%@__%@", @"ATTACHMENT_ID", @"ATTACHMENT_UUID", @"img.jpg"] lowercaseString]];
  request.URL = [NSURL URLWithString:urlString];

  NSMutableURLRequest *request2 = [[NSMutableURLRequest alloc] initWithURL:[NSURL URLWithString:urlString]];
  [request2 setHTTPMethod:@"PUT"];
  [request2 setAllHTTPHeaderFields:[request allHTTPHeaderFields]];
  [request2 setValue:@"BUCKET_NAME.s3-eu-west-1.amazonaws.com" forHTTPHeaderField:@"Host"];

  NSURLSessionUploadTask *uploadTask = [session uploadTaskWithRequest:request2 fromFile:fromFile];
  [uploadTask resume];
}

#pragma - NSURLSessionTaskDelegate

- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didSendBodyData:(int64_t)bytesSent totalBytesSent:(int64_t)totalBytesSent totalBytesExpectedToSend:(int64_t)totalBytesExpectedToSend {
    dispatch_async(dispatch_get_main_queue(), ^{
      self.progressView.progress = (float)totalBytesSent / (float) totalBytesExpectedToSend;
    });
}

- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
  NSURLSessionConfiguration *sessionConfig = [session configuration];
  NSString *identifier = [sessionConfig identifier];

  NSLog(@"**identifier**: %@", identifier);

  NSArray *identifierComponents = [identifier componentsSeparatedByString:@"."];
  NSString *lastComponent = [identifierComponents lastObject];
  NSArray *components = [lastComponent componentsSeparatedByString:@"-"];
  NSString *sessionType = [components firstObject];
  NSString *attachmentID = [components lastObject];

  NSLog(@"sessionType: %@", sessionType);
  NSLog(@"attachmentID: %@", attachmentID);

  if (error == nil)
  {
    NSLog(@"Task %@ completed successfully", task);
    if ([sessionType isEqualToString:@"S3"]) {
        NSString *downloadIdentifier = [NSString stringWithFormat:@"com.journeyhq.backgroundSession.%@-%@", @"s3Completion", attachmentID];
        NSURLSessionConfiguration *sessionConfiguration = [NSURLSessionConfiguration backgroundSessionConfiguration:downloadIdentifier];
        NSURLSession *downloadSession = [NSURLSession sessionWithConfiguration:sessionConfiguration delegate:self delegateQueue:nil];

        NSString *urlString = @"API_COMPLETION_URL";
        NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:[NSURL URLWithString:urlString]];

        NSURLSessionDownloadTask *downloadTask = [downloadSession downloadTaskWithRequest:request];
        [downloadTask resume];
    }
  }
  else
  {
    NSLog(@"Task %@ completed with error: %@", task,
          [error localizedDescription]);
  }
  task = nil;

}

AppDelegate.h

- (void)application:(UIApplication *)application
handleEventsForBackgroundURLSession:(NSString *)identifier
completionHandler:(void (^)())completionHandler {
}

EDIT I have also tried the following. The task get created as before, still doesn't fire when I resume the task.

- (void)application:(UIApplication *)application
handleEventsForBackgroundURLSession:(NSString *)identifier
completionHandler:(void (^)())completionHandler {

    NSDictionary *userInfo = @{
        @"completionHandler" : completionHandler,
        @"sessionIdentifier" : identifier
    };

    [[NSNotificationCenter defaultCenter]
        postNotificationName:@"BackgroundTransferNotification"
        object:nil
        userInfo:userInfo];
}

Then in the view controller

- (void)viewWillAppear:(BOOL)animated {
    [super viewWillAppear:animated];

    [[NSNotificationCenter defaultCenter]
     addObserver:self
     selector:@selector(handleBackgroundTransfer:)
     name:@"BackgroundTransferNotification"
     object:nil];
}

- (void)handleBackgroundTransfer:(NSNotification *)notification {
    // handle the API call as shown in original example
    ...
}
Community
  • 1
  • 1
Steve P. Sharpe
  • 129
  • 1
  • 7

2 Answers2

1

As the documentation for NSURLSessionDidFinishEventsForBackgroundURLSession says:

In iOS, when a background transfer completes or requires credentials, if your app is no longer running, your app is automatically relaunched in the background, and the app’s UIApplicationDelegate is sent an application:handleEventsForBackgroundURLSession:completionHandler: message. This call contains the identifier of the session that caused your app to be launched. Your app should then store that completion handler before creating a background configuration object with the same identifier, and creating a session with that configuration. The newly created session is automatically reassociated with ongoing background activity.

When your app later receives a URLSessionDidFinishEventsForBackgroundURLSession: message, this indicates that all messages previously enqueued for this session have been delivered, and that it is now safe to invoke the previously stored completion handler or to begin any internal updates that may result in invoking the completion handler.

So, handleEventsForBackgroundURLSession should save the completionHandler and reinstantiate background NSURLSession. Then, URLSessionDidFinishEventsForBackgroundURLSession should call that completionHandler.

I don't see you re-instantiating the background session anywhere when your app is reawakened and handleEventsForBackgroundURLSession is called. Nor do I see you implement a URLSessionDidFinishEventsForBackgroundURLSession that will call that completionHandler.

Rob
  • 371,891
  • 67
  • 713
  • 902
0

If a background task is finished while your app is not running you will have to handle it in your AppDelegate, which you don't seem to be doing.

Something like this should work:

- (void)application:(UIApplication *)application handleEventsForBackgroundURLSession:(NSString *)identifier completionHandler:(void (^)())completionHandler {

    // the identifier you used to create the background session
    NSString *identifier = [NSString stringWithFormat:@"com.journeyhq.backgroundSession.%@-%@", @"S3", @"ATTACHMENT_REMOTE_ID"];

    if ([identifier isEqualToString:identifier]) {
        // call your API here
    }

    // call the handler block so the app can exit again
    completionHandler();    
}
Hugo Sousa
  • 113
  • 1
  • 5
  • Thanks for the reply Hugo. I did try this before, although not directly inside the app delegate, but via a notification triggered inside handleEventsForBackgroundURLSession that then gets handled in a view controller. Unfortunately it had the same outcome. It creates the task, however it just doesn't fire, even though I call resume on the task, unless I open the app, then the task runs. I tried it directly inside handleEventsForBackgroundURLSession and it still doesn't fire. – Steve P. Sharpe Apr 07 '14 at 19:57
  • 2
    Can you please post the code you tried inside the app delegate method? – Hugo Sousa Apr 09 '14 at 23:15
  • 1
    Did you ever solve this? I'm having exactly the same issue – d0n13 Mar 03 '16 at 11:58