Turn Any iOS Task into a Background Task with ReactiveCocoa’s RACSignal

If an app performs a task that needs to complete even if the app is moved to the background, iOS provides a way to request a little more time to complete the task. You just need to let iOS know when you’ve started the task and when it has ended. If the app is moved to the background while the task is still in progress, iOS will allow the app to keep running (up to 3 minutes in iOS 7 and 8) to try to give the task a chance to complete.

I’ve found that by wrapping the necessary iOS begin/end calls in a RACSignal, I can prevent having them littered throughout the codebase, and no longer have to worry about remembering to let iOS know when a task has ended. In this post I’ll provide an example of how this can be done.

h2. Executing Finite-Length Tasks

In Apple’s documentation on “Background Execution”:https://developer.apple.com/library/ios/documentation/iPhone/Conceptual/iPhoneOSProgrammingGuide/BackgroundExecution/BackgroundExecution.html, the ability to execute finite-length tasks is described this way:

bq. Apps moving to the background are expected to put themselves into a quiescent state as quickly as possible so that they can be suspended by the system. If your app is in the middle of a task and needs a little extra time to complete that task, it can call the beginBackgroundTaskWithName:expirationHandler: or beginBackgroundTaskWithExpirationHandler: method of the UIApplication object to request some additional execution time. Calling either of these methods delays the suspension of your app temporarily, giving it a little extra time to finish its work. Upon completion of that work, your app must call the endBackgroundTask: method to let the system know that it is finished and can be suspended.

That same section of the documentation points out that you don’t need to wait until the app is about to move to the background to designate a background task. In fact, it says, it’s best to designate a background task before starting the task and to end it as soon as it finishes—regardless of whether the app ever moved to the background during the task’s execution.

A pattern I’ve seen in a number of places is to make the call to @beginBackgroundTaskWithExpirationHandler:@ and store the returned UIBackgroundTaskIdentifier in a property which will then be passed to @endBackgroundTask:@ when the task is complete. In a non-Reactive Cocoa app that might look something like:

- (void)saveChanges {
    self.backgroundTask = 
        [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{
            self.backgroundTask = UIBackgroundTaskInvalid;
        }];

    self.repository.delegate = self;
    
    // Do some asychronous work that will call the 
    // delegate method "didCompleteSave" when done
    // or "didFailToSave" on failure
    [self.repository save]
}

- (void)didCompleteSave {
    if (self.backgroundTask != UIBackgroundTaskInvalid) {
        [[UIApplication sharedApplication] endBackgroundTask:self.backgroundTask];
        self.backgroundTask = UIBackgroundTaskInvalid;
    }
}

- (void)didFailToSave {
  if (self.backgroundTask != UIBackgroundTaskInvalid) {
      [[UIApplication sharedApplication] endBackgroundTask:self.backgroundTask];
      self.backgroundTask = UIBackgroundTaskInvalid;
  }
}

With this type of implementation you’ve introduced additional framework specific state (the @backgroundTask@) that needs to be accounted for in multiple places (both in the success and failure case). Even when using a block-based implementation, you’d always have to remember to deal with the background task no matter what execution path the app followed.

h2. The Task as a RACSignal

I’ve found that wrapping tasks in RACSignals to be a very powerful alternative to using delegate/callback or block-based approaches. Using some of the concepts I discussed in a previous post, “ReactiveCocoa Concepts for Asynchronous Libraries”:https:/reactivecocoa-asynchronous-libraries/, you can create a @RACSignal@ that when subscribed to will have the side-effect of performing whatever work needs to be done, asynchronous or not. One nice benefit of using a RACSignal as the interface to your task is that there is a uniform way of indicating completion or an error.

If the @save@ method on the @repository@ from the example above returned a @RACSignal@ instead, we could do something like:

- (void)saveChanges {
    UIBackgroundTaskIdentifier backgroundTask = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{
        NSLog(@"Ran out of time");
    }];

    RACSignal *taskSignal = [self.repository save];

    [taskSignal subscribeError:^(NSError *error) {
        [[UIApplication sharedApplication] endBackgroundTask:backgroundTask];
    } completed:^{
        [[UIApplication sharedApplication] endBackgroundTask:backgroundTask];
    }];
}

This is a little nicer because now there’s no need for storing the backgroundTask value in a property. But we still have to put the call to @endBackgroundTask:@ in two places. And even worse, from a ReactiveCocoa standpoint, this works by subscribing to the signal, which means the signal can’t be used as part of a larger signal composition before being subscribed to.

h3. Merging the Behavior

A different approach is to completely extract the background task logic into a separate signal, and merge that with the signal that is performing the task. The following utility method will return a @RACSignal@ that when subscribed to will tell iOS that a background task has started, and when the subscription is disposed of will tell iOS that the background task has ended. It wraps the passed in signal (and couple be written as an operator on @RACSignal@ itself):

@implementation BackgroundUtils

+ (RACSignal *)backgroundTaskSignal:(RACSignal *)sourceSignal {
    RACSignal *backgroundSignal = [RACSignal createSignal:^RACDisposable *(id  subscriber) {
        UIBackgroundTaskIdentifier backgroundTask =
            [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{

                NSError *error = [NSError errorWithDomain:@"MyAppDomain"
                                                     code:-42
                                                 userInfo:nil];
                [subscriber sendError:error];
            }];
            
        // End the background task when the subscription
        // is disposed of    
        return [RACDisposable disposableWithBlock:^{
            [[UIApplication sharedApplication] endBackgroundTask:backgroundTask];
        }];
    }];

    RACSignal *taskSignal = [sourceSignal
        then:^RACSignal * {
            // Need to send a value so the "take:1" will see it
            return [RACSignal return:[RACUnit defaultUnit]];
        }];

    // Combine the background task behavior with
    // whatever the task being performed is
    return [[RACSignal merge:@[backgroundSignal, taskSignal]] take:1];
}  

@end

As you can see, the call to @endBackgroundTask:@ is in the dispose block. This means that no matter why the subscription was disposed of, the call to end the background task will be made. When the source signal completes, a @RACUnit@ is sent that will cause the @take:1@ to complete the combined signal—resulting in the subscription being disposed of. Alternatively, if the source signal sends an error, that will also cause the subscription to be disposed of.

We can now update the earlier @saveChanges@ method to make the save functionality a background task. And by returning a @RACSignal@ from the method, instead of subscribing immediately, this behavior can be composed into a more complex signal before being subscribed to if necessary:

- (RACSignal *)saveChanges {
    return [BackgroundUtils backgroundTaskSignal:[self.repository save]];
}

We can now wrap any task the app needs to perform, whether it be synchronous or asynchronous, and turn it into a background task without having to make any changes to the code in the task itself.

Conversation
  • ANUP says:

    @patrick…where is the implementation for save method?

    • Patrick Bacon Patrick Bacon says:

      For the purposes of this post it doesn’t actually matter what the save method does, as long as it returns a RACSignal. So for clarity, you might have something like:

      
      - (RACSignal *)save {
          return [RACSignal defer:^{
      
              // Code for saving something goes here
      
              return [RACSignal empty];
          });
      }
      
      • ANUP says:

        @patrick….To which class do self.repository belongs??Thank you.

        • Patrick Bacon Patrick Bacon says:

          There isn’t actually a class for the repository. It’s just a made up example of a class that might have an asynchronous API for something that you might want to do while the app is in the background.

  • Comments are closed.