iOS Components and Frameworks: Understanding the Advanced Features of the iOS SDK (2014)

Chapter 16. Working with Background Tasks

When iOS was first introduced in 2008, only one third-party app at a time could be active—the foreground app. This meant that any tasks that the app needed to complete had to finish while the app was in the foreground being actively used, or those tasks had to be paused and resumed the next time the app was started. With the introduction of iOS 4, background capabilities were added for third-party apps. Since iOS devices have limited system resources and battery preservation is a priority, background processing has some limitations to prevent interference with the foreground app and to prevent using too much power. An app can accomplish a lot with correct usage of backgrounding capabilities. This chapter explains what options are available and how to use them.

There are two approaches to background task processing supported by iOS:

Image The first approach is finishing a long-running task in the background. This method is appropriate for completing things like large downloads or data updates that stretch beyond the time the user spends interacting with the app.

Image The second approach is supporting very specific types of background activities allowed by iOS, such as playing music, interacting with Bluetooth devices, monitoring the GPS for large changes, or maintaining a persistent network connection to allow VoIP-type apps to work.


Note

The term “background task” is frequently used interchangeably for two different meanings: executing a task when the app is not in the foreground (described in this chapter), and executing a task asynchronously off the main thread (described in Chapter 17, “Grand Central Dispatch for Performance”).


The Sample App

The sample app for this chapter is called BackgroundTasks. This app demonstrates the two backgrounding approaches: completing a long-running task while the app is in the background, and playing audio continuously while the app is in the background. The user interface is very simple—it presents a button to start and stop background music, and a button to start the background task (see Figure 16.1).

Image

Figure 16.1 BackgroundTasks sample app.

Checking for Background Availability

All devices capable of running iOS 6 or iOS 5 support performing activities in the background, referred to as multitasking in Apple’s documentation. If the target app needs to support iOS 4, note that there are a few devices that do not support multitasking. Any code written to take advantage of multitasking should check to ensure that multitasking is supported by the device. When the user taps the Start Background Task button in the sample app, the startBackgroundTaskTouched: method in ICFViewController will be called to check for multitasking support.

- (IBAction)startBackgroundTaskTouched:(id)sender
{
    UIDevice* device = [UIDevice currentDevice];

    if (! [device isMultitaskingSupported])
    {
        NSLog(@"Multitasking not supported on this device.");
        return;
    }

    [self.backgroundButton setEnabled:NO];
    NSString *buttonTitle =@"Background Task Running";

    [self.backgroundButton setTitle:buttonTitle
                           forState:UIControlStateNormal];

    dispatch_queue_t background =
    dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

    dispatch_async(background, ^{
        [self performBackgroundTask];
    });

}

To check for multitasking availability, use the class method currentDevice on UIDevice to get information about the current device. Then call the isMultitaskingSupported method to determine whether multitasking is supported. If multitasking is supported, the user interface is updated and the performBackgroundTask method is called asynchronously to start the background task (refer to Chapter 17 for more info on executing tasks asynchronously).

Finishing a Task in the Background

To execute a long-running task in the background, the application needs to be informed that the task should be capable of running in the background. Consideration should also be given to the amount of memory needed for the task, and the total amount of time needed to complete the task. If the task will be longer than 10 to 15 minutes, it is very likely that the app will be terminated before the task can complete. The task logic should be able to handle a quick termination, and be capable of being restarted when the app is relaunched. Apps are given a set amount of time to complete their background tasks, and can be terminated earlier than the given amount of time if the operating system detects that resources are needed.

To see the background task in action, start the sample app as shown in Figure 16.1 from Xcode. Tap the Start Background Task button, and then tap the Home button to exit the app. Observe the console in Xcode to see log statements generated by the app; this will confirm that the app is running in the background.

Background task starting, task ID is 1.
Background Processed 0. Still in foreground.
Background Processed 1. Still in foreground.
Background Processed 2. Still in foreground.
Background Processed 3. Still in foreground.
Background Processed 4. Still in foreground.
Background Processed 5. Still in foreground.
Background Processed 6. Time remaining is: 599.579052
Background Processed 7. Time remaining is: 598.423525
Background Processed 8. Time remaining is: 597.374849
Background Processed 9. Time remaining is: 596.326780
Background Processed 10. Time remaining is: 595.308253

The general process to execute a background task is as follows:

Image Request a background task identifier from the application, specifying a block as an expiration handler.

The expiration handler will get called only if the app runs out of background time or the system determines that resource usage is too high and the app should be terminated.

Image Perform the background task logic.

Any code between the request for background task identifier and the background task end will be included.

Image Tell the application to end the background task, and invalidate the background task identifier.

Background Task Identifier

To start a task that can complete running in the background, a background task identifier needs to be obtained from the application. The background task identifier helps the application keep track of which tasks are running and which are complete. The background task identifier is needed to tell the application that a task is complete and background processing is no longer required. The performBackgroundTask method in ICFViewController obtains the background task identifier before beginning the work to be done in the background.

__block UIBackgroundTaskIdentifier bTask =
[[UIApplication sharedApplication]
beginBackgroundTaskWithExpirationHandler:
^{
    ...
}];

When obtaining a background task, an expiration handler block should be specified. The reason the __block modifier is used when declaring the background task identifier is that the background task identifier is needed in the expiration handler block, and needs to be modified in the expiration handler block.

Expiration Handler

The expiration handler for a background task will get called if the operating system decides the app has run out of time and/or resources and needs to be shut down. The expiration handler will get called on the main thread before the app is about to be shut down. Not much time is provided (at most it is a few seconds), so the handler should do a minimal amount of work.

__block UIBackgroundTaskIdentifier bTask =
[[UIApplication sharedApplication]
beginBackgroundTaskWithExpirationHandler:
^{
    NSLog(@"Background Expiration Handler called.");
    NSLog(@"Counter is: %d, task ID is %u.",counter,bTask);

    [[UIApplication sharedApplication]
    endBackgroundTask:bTask];

    bTask = UIBackgroundTaskInvalid;
}];

The minimum that the expiration handler should do is to tell the application that the background task has ended by calling the endBackgroundTask: method on the shared application instance, and invalidate the background task ID by setting the bTask variable toUIBackgroundTaskInvalid so that it will not inadvertently be used again.

Background Processed 570. Time remaining is: 11.482063
Background Processed 571. Time remaining is: 10.436456
Background Processed 572. Time remaining is: 9.394706
Background Processed 573. Time remaining is: 8.346616
Background Processed 574. Time remaining is: 7.308527
Background Processed 575. Time remaining is: 6.260324
Background Processed 576. Time remaining is: 5.212251
Background Expiration Handler called.
Counter is: 577, task ID is 1.

Completing the Background Task

After a background task ID has been obtained, the actual work to be done in the background can commence. In the performBackgroundTask method some variables are set up to know where to start counting, to keep count of iterations, and to know when to stop. A reference toNSUserDefaults standardUserDefaults is established to retrieve the last counter used, and store the last counter with each iteration.

NSInteger counter = 0;

NSUserDefaults *userDefaults =
[NSUserDefaults standardUserDefaults];

NSInteger startCounter =
[userDefaults integerForKey:kLastCounterKey];

NSInteger twentyMins = 20 * 60;

The background task for the sample app is trivial—it puts the thread to sleep for a second in a loop to simulate lots of long-running iterations. It stores the current counter for the iteration in NSUserDefaults so that it will be easy to keep track of where it left off if the background task expires. This logic could be modified to handle keeping track of any repetitive background task.

NSLog(@"Background task starting, task ID is %u.",bTask);
for (counter = startCounter; counter<=twentyMins; counter++)
{
    [NSThread sleepForTimeInterval:1];
    [userDefaults setInteger:counter
                      forKey:kLastCounterKey];

    [userDefaults synchronize];

    NSTimeInterval remainingTime =
    [[UIApplication sharedApplication] backgroundTimeRemaining];

    NSLog(@"Background Processed %d. Time remaining is: %f",
    counter,remainingTime);
}

When each iteration is complete, the time remaining for the background task is obtained from the application. This can be used to determine whether additional iterations of a background task should be started.


Note

The background task is typically expired when there are a few seconds remaining to give it time to wrap up, so any decision to start a new iteration should take that into consideration.


After the background task work is done, the last counter is updated in NSUserDefaults so that it can start over correctly, and the UI is updated to allow the user to start the background task again.

NSLog(@"Background Completed tasks");

[userDefaults setInteger:0
                  forKey:kLastCounterKey];

[userDefaults synchronize];

dispatch_sync(dispatch_get_main_queue(), ^{
    [self.backgroundButton setEnabled:YES];
    [self.backgroundButton setTitle:@"Start Background Task"
                           forState:UIControlStateNormal];
});

Finally there are two key items that need to take place to finish the background task: tell the application to end the background task, and invalidate the background task identifier. Every line of code between obtaining the background task ID and ending it will execute in the background.

[[UIApplication sharedApplication] endBackgroundTask:self.backgroundTask];

self.backgroundTask = UIBackgroundTaskInvalid;

Implementing Background Activities

iOS supports a very specific set of background activities that can continue processing without the limitations of the background task identifier approach. These activities can continue running or being available without a time limit, and need to avoid using too many system resources to keep the app from getting terminated.

Types of Background Activities

These are the background activities:

Image Playing background audio

Image Tracking device location

Image Supporting a Voice over IP app

Image Downloading new Newsstand app content

Image Communication with an external or Bluetooth accessory

Image Fetching content in the background

Image Initiating a background download with a push notification

To support any of these background activities, the app needs to declare which background activities it supports in the Info.plist file. To do this, select the app target in Xcode, and select the Capabilities tab. Turn “Background Modes” to On, then check the desired modes to support. Xcode will add the entries to the Info.plist file for you. Or, edit the Info.plist file directly by selecting the app target in Xcode, then select the Info tab. Look for the Required Background Modes entry in the list; if it is not present, hover over an existing entry and click the plus sign to add a new entry, and then select Required Background Modes. An array entry will be added with one empty NSString item. Select the desired background mode, as shown in Figure 16.2.

Image

Figure 16.2 Xcode’s Info editor showing required background modes.

After the Required Background Modes entry is established, activity-specific logic can be built into the app and it will function when the app is in the background.

Playing Music in the Background

To play music in the background, the first step is to adjust the audio session settings for the app. By default, an app uses the AVAudioSessionCategorySoloAmbient audio session category. This ensures that other audio is turned off when the app is started, and that the app audio is silenced when the screen is locked or the ring/silent switch on the device is set to silent. This session will not work, since audio will be silenced when the screen is locked or when another app is brought to the foreground. The viewDidLoad method in ICFViewController adjusts the audio session category to AVAudioSessionCategoryPlayback, which will ensure that the audio will continue playing when the app is in the background or the ring/silent switch is set to silent.

AVAudioSession *session = [AVAudioSession sharedInstance];

NSError *activeError = nil;
if (![session setActive:YES error:&activeError])
{
    NSLog(@"Failed to set active audio session!");
}

NSError *categoryError = nil;
if (![session setCategory:AVAudioSessionCategoryPlayback
                   error:&categoryError])
{
    NSLog(@"Failed to set audio category!");
}

The next step in playing audio is to initialize an audio player. This is also done in the viewDidLoad method so that the audio player is ready whenever the user indicates that audio should be played.

NSError *playerInitError = nil;

NSString *audioPath =
[[NSBundle mainBundle] pathForResource:@"16_audio"
                                ofType:@"mp3"];

NSURL *audioURL = [NSURL fileURLWithPath:audioPath];

self.audioPlayer = [[AVAudioPlayer alloc]
                    initWithContentsOfURL:audioURL
                    error:&playerInitError];

The Play Background Music button is wired to the playBackgroundMusicTouched: method. When the user taps that button, the logic checks to see whether audio is currently playing. If audio is currently playing, the method stops the audio and updates the title of the button.

if ([self.audioPlayer isPlaying])
{
    [self.audioPlayer stop];

    [self.audioButton setTitle:@"Play Background Music"
                      forState:UIControlStateNormal];

}
else
{ ...
}

If music is not currently playing, the method starts the audio and changes the title of the button.

[self.audioPlayer play];

[self.audioButton setTitle:@"Stop Background Music"
                  forState:UIControlStateNormal];

While the audio is playing, the user can press the lock button on the device or the home button to background the app, and the audio will continue playing. A really nice feature when audio is playing and the screen is locked is to display the currently playing information on the lock screen. To do this, first set up a dictionary with information about the playing media.

UIImage *lockImage = [UIImage imageNamed:@"book_cover"];

MPMediaItemArtwork *artwork =
[[MPMediaItemArtwork alloc] initWithImage:lockImage];

NSDictionary *mediaDict =
@{
    MPMediaItemPropertyTitle: @"BackgroundTask Audio",
    MPMediaItemPropertyMediaType: @(MPMediaTypeAnyAudio),
    MPMediaItemPropertyPlaybackDuration:
    @(self.audioPlayer.duration),
    MPNowPlayingInfoPropertyPlaybackRate: @1.0,
    MPNowPlayingInfoPropertyElapsedPlaybackTime:
    @(self.audioPlayer.currentTime),
    MPMediaItemPropertyAlbumArtist: @"Some User",
    MPMediaItemPropertyArtist: @"Some User",
    MPMediaItemPropertyArtwork: artwork };

There are a number of options that can be set. Note that a title and an image are specified; these will be displayed on the lock screen. The duration and current time are provided and can be displayed at the media player’s discretion, depending on the state of the device and the context in which it will be displayed. After the media information is established, the method starts the audio player, then informs the media player’s MPNowPlayingInfoCenter about the playing media item info. It sets self to be the first responder, since the media player’s info center requires the view or view controller playing audio to be the first responder in order to work correctly. It tells the app to start responding to “remote control” events, which will allow the lock screen controls to control the audio in the app with delegate methods implemented.

[[MPNowPlayingInfoCenter defaultCenter]
 setNowPlayingInfo:mediaDict];

[self becomeFirstResponder];

[[UIApplication sharedApplication]
 beginReceivingRemoteControlEvents];

Now when the audio is playing in the background, the lock screen will display information about it, as shown in Figure 16.3.

Image

Figure 16.3 Lock screen showing sample app playing background audio.

In addition, the Control Center will display information about the audio, as shown in Figure 16.4.

Image

Figure 16.4 Lock screen control center showing sample app playing background audio.

Summary

This chapter illuminated two approaches to executing tasks while the app is in the background, or while it is not the current app that the user is interacting with.

The first approach to background task processing described was finishing a long-running task in the background. The sample app demonstrated how to check whether the device is capable of running a background task, showed how to set up and execute a background task, and explained how to handle the case when iOS terminates the app and notifies the background task that it will be ended (or expired).

The second approach to background tasks described was supporting very specific types of background activities allowed by iOS, like playing music, interacting with Bluetooth devices, monitoring the GPS for large changes, or maintaining a persistent network connection to allow VoIP-type apps to work. The chapter explained how to configure an Xcode project to allow a background activity to take place, and then the sample app demonstrated how to play audio in the background while displaying information about the audio on the lock screen.

Exercises

1. Enhance the background task processing to download a series of images, or download and process a large JSON file in the background.

2. Add the capability to monitor the GPS for large location changes while the app is in the background, and log the changes as they happen. Refer to Chapter 2, “Core Location, MapKit, and Geofencing,” for more information on location information.

3. Update the audio handling to support the controls shown on the lock screen and Control Center. Hint: Implement the remoteControlReceivedWithEvent: method in the view controller that becomes the first responder.