Using Background Fetch in iOS

iOS Development

In iOS, except for some special cases, an application is not allowed to run in the background. While certainly a serious restriction for some types of application, this feature is designed to conserve battery power. Imagine apps continuing to run in the background without reins, and it is easy to see how the entire system might slow down and deplete the battery within a few hours.

Apple takes a measured approach to background processing. In the early days of the iPhone, after complaints from developers about the inability to run apps in the background, Apple introduced the Apple Push Notification service (APNS). APNs allows apps which are not already running to receive background push notifications, thereby allowing apps to be notified when some important events need their attention.

In iOS 7, Apple has introduced another feature to allow apps to run (more or less) in the background – Background Fetch. Background Fetch allows your app to continuously fetch data from the network when it is in the background. As an example, consider the Facebook app. When the Facebook app is in the background, it can continually fetch the latest news feed so that when the user switches the app to the foreground, the latest news feed is already ready for viewing.

In this article we’ll take a look to see how background fetch can be used to connect to a Web service, the Open Weather Map API, so that your app can continue to update itself in the background.

Background Fetch Frequency

How frequent your application can perform a background fetch is determined by the system. It depends on factors such as:

  • Whether network connectivity is available at that particular time
  • Whether the device is awake
  • How much data and time your application has taken in its previous attempt to perform a background fetch

In other words, your application is entirely at the mercy of the system to schedule background fetch for you. In addition, each time your application uses background fetch, it has at most 30 seconds to complete the process.

Implementing Background Fetch

Let’s now see how Background Fetch works. First, using Xcode, create a Single View application and name it as shown in Figure 1.

ios-fetch-1

Once the project is created, select the project name and click on the Capabilities tab on the right (see Figure 2). Turn on the Background Modes capability and expand it. Check the Background fetch item.

ios-fetch-2

In the BGAppRefreshAppDelegate.m file, add in the following statements:

#import "BGAppRefreshAppDelegate.h"

@implementation BGAppRefreshAppDelegate

-(void)               application:(UIApplication *)application
performFetchWithCompletionHandler:
(void (^)(UIBackgroundFetchResult))completionHandler {

    NSLog(@"Background fetch started...");

    //---do background fetch here---
    // You have up to 30 seconds to perform the fetch

    BOOL downloadSuccessful = YES;

    if (downloadSuccessful) {
        //---set the flag that data is successfully downloaded---
        completionHandler(UIBackgroundFetchResultNewData);
    } else {
        //---set the flag that download is not successful---
        completionHandler(UIBackgroundFetchResultFailed);
    }

    NSLog(@"Background fetch completed...");
}

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    [[UIApplication sharedApplication]
        setMinimumBackgroundFetchInterval:
            UIApplicationBackgroundFetchIntervalMinimum];

    // Override point for customization after application launch.
    return YES;
}

In the application:didFinishLaunchingWithOptions: method, you indicate to the system that your app wishes to perform background fetch by calling the setMinimumBackgroundFetchInterval: method of the UIApplication object. This method takes in a double value indicating the minimum number of seconds to wait between background fetches. This value that you specify is only indicative and have no real effects on the scheduling frequency (remember, the system determines that for your application). You set this using the UIApplicationBackgroundFetchIntervalMinimum constant, which is the shortest fetch interval supported by the system. If you want to stop background fetch, use the UIApplicationBackgroundFetchIntervalNever constant.

You also implemented the application:performFetchWithCompletionHandler: method, which is called whenever your app performs a background fetch. At this moment, the method is not doing anything useful, and we will see in the next section how to perform a network connection. For now, you need to know that you have up to 30 seconds to perform your background fetch operation, and that you need to call the completionHandler block as soon as your application has finished downloading data. Doing so allows the system to collect usage statistics such as data cost and power consumption, and all these data are used to give your app some opportunity to perform background fetch in the future.

To test if background fetch really works, you need some help from Xcode. First, deploy the application onto the iPhone Simulator. Because you don’t really know when the system will schedule your app for background fetch, you need to select the Debug > Simulate Background Fetch menu item in Xcode (see Figure 3). Your application on the iPhone Simulator will now go into the background.

ios-fetch-3

Once you have used Xcode to simulate a background fetch for your app, look at the Output window (press Cmd-Shift-C if you don’t see it). You should see the printout as shown in Figure 4. The two statements printed confirm that the background fetch method has been called.

Performing a Network Operation

Let’s now learn how to perform a network operation during a background fetch. For this example, you shall connect to a Web service that returns the weather information for a specific location. The weather Web service that will be used is the OpenWeatherMap API. Once the weather information is obtained, you shall display it in the main View Controller of the application.

First, add a Label view to the View window in the Main.Storyboard file (see Figure 5). Make sure that it can display multiple lines (at least 3).

ios-fetch-5

Create an outlet for the Label view and name it as lblStatus in the BGAppRefreshViewController.h file:

#import <UIKit/UIKit.h>

@interface BGAppRefreshViewController : UIViewController

@property (weak, nonatomic) IBOutlet UILabel *lblStatus;

@end

Add the following statements to the BGAppRefreshAppDelegate.m file:

#import "BGAppRefreshAppDelegate.h"
#import "BGAppRefreshViewController.h"

@interface BGAppRefreshAppDelegate ()

@property (nonatomic, strong) NSString *temperature;

@end

@implementation BGAppRefreshAppDelegate

-(void)               application:(UIApplication *)application
performFetchWithCompletionHandler:
(void (^)(UIBackgroundFetchResult))completionHandler {

    //---do background fetch here---
    // You have up to 30 seconds to perform the fetch

    /*
    BOOL downloadSuccessful = YES;

    if (downloadSuccessful) {
        //---set the flag that data is successfully downloaded---
        completionHandler(UIBackgroundFetchResultNewData);
    } else {
        //---set the flag that download is not successful---
        completionHandler(UIBackgroundFetchResultFailed);
    }
    */

    NSLog(@"Background fetch started...");
    NSString *urlString = [NSString stringWithFormat:
        @"http://api.openweathermap.org/data/2.5/weather?q=%@", 
        @"Singapore"];

    NSURLSession *session = [NSURLSession sharedSession];
    [[session dataTaskWithURL:[NSURL URLWithString:urlString]
            completionHandler:^(NSData *data,
                                NSURLResponse *response,
                                NSError *error) {
                NSHTTPURLResponse *httpResp = (NSHTTPURLResponse*) response;
                if (!error && httpResp.statusCode == 200) {
                    //---print out the result obtained---
                    NSString *result =
                    [[NSString alloc] initWithBytes:[data bytes]
                                             length:[data length]
                                           encoding:NSUTF8StringEncoding];
                    NSLog(@"%@", result);

                    //---parse the JSON result---
                    [self parseJSONData:data];

                    //---update the UIViewController---
                    BGAppRefreshViewController *vc =   
                        (BGAppRefreshViewController *)
                        [[[UIApplication sharedApplication] keyWindow]        
                        rootViewController];
                    dispatch_sync(dispatch_get_main_queue(), ^{
                        vc.lblStatus.text = self.temperature;
                    });

                    completionHandler(UIBackgroundFetchResultNewData);
                    NSLog(@"Background fetch completed...");
                } else {
                    NSLog(@"%@", error.description);
                    completionHandler(UIBackgroundFetchResultFailed);
                    NSLog(@"Background fetch Failed...");
                }
            }
     ] resume
    ];
}

- (void)parseJSONData:(NSData *)data {
    NSError *error;
    NSDictionary *parsedJSONData =
    [NSJSONSerialization JSONObjectWithData:data
                                    options:kNilOptions
                                      error:&error];
    NSDictionary *main = [parsedJSONData objectForKey:@"main"];

    //---temperature in Kelvin---
    NSString *temp = [main valueForKey:@"temp"];

    //---convert temperature to Celcius---
    float temperature = [temp floatValue] - 273;

    //---get current time---
    NSDate *date = [NSDate date];
    NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
    [formatter setDateFormat:@"HH:mm:ss"];

    NSString *timeString = [formatter stringFromDate:date];

    self.temperature = [NSString stringWithFormat:
        @"%f degrees Celsius, fetched at %@",
        temperature, timeString];
}

The temperature property is used to store the temperature returned by the Web service.

Note that we specify Web service API that we are using is http://api.openweathermap.org/data/2.5/weather?q=Singapore, and this specifies Singapore as the geographical area that we are interested in. You can copy this URL into your browser to see first-hand what the API’s JSON response looks like. You can also change the location by specifying q=city,country in the URL.

To connect to the Web service, you will use the new NSURLSession class introduced in iOS 7 (I will have a more detailed discussion of this in another article) to asynchronously connect to the Web service. When the Web service returns the data, you will then call the parseJSONData: method to extract the temperature readings from the JSON string and then save it using the application preferences. A sample JSON data looks like this:

{
     "coord" : {
        "lon":-0.13,
        "lat":51.51
     },
     "sys" : {
        "message":0.0069,
        "country":"GB",
        "sunrise":1384413482,
        "sunset":1384445522
     },
     "weather":[
     {
        "id":802,
        "main":"Clouds",
        "description":"scattered clouds",
        "icon":"03d"
     }
     ],
     "base":"gdps stations",
     "main" : {
        "temp":282.57,
        "pressure":1022,
        "humidity":61,
        "temp_min":282.15,
        "temp_max":283.15
     },
     "wind" : {
        "speed":7.7,
        "deg":310
     },
     "rain" : {
        "3h":0
     },
     "clouds" : {
        "all":40
     },
     "dt":1384429800,
     "id":2643743,
     "name":"London",
     "cod":200
 }

When the Web service has returned the data, you will get a reference to the main View Controller in your application and update the Label view to display the weather information that you have obtained:

                    //---update the UIViewController---
                    BGAppRefreshViewController *vc =   
                        (BGAppRefreshViewController *)
                        [[[UIApplication sharedApplication] keyWindow]        
                        rootViewController];
                    dispatch_sync(dispatch_get_main_queue(), ^{
                        vc.lblStatus.text = self.temperature;
                    });

                    completionHandler(UIBackgroundFetchResultNewData);
                    NSLog(@"Background fetch completed...");

You call the completionHandler block with the UIBackgroundFetchResultNewData constant to inform the system that you have fetched new data. One side effect of this is that the system will take a snap shot of the UI of your application so that when the user switches back to your application your updated UI is displayed immediately. Interestingly, your UI will be updated even though it is invisible and in the background.

The system will save the snapshot in the Library/Caches/Snapshots/<application_name>/Main/ folder of the application sandbox. On the Simulator, you can verify this by going to the folder:

    “~/Library/Application Support/iPhone Simulator/<iOS_version_no>/Applications/<application_sandbox>/Library/Caches/Snapshots/<application_name>/Main/”

You will find a captured snapshot of your application in this folder.

To test the application, run the application on the iPhone Simulator. When the app is first launched, you will see an empty Label view as shown in Figure 6.

ios-fetch-6

Back in Xcode, select the Debug > Simulate Background Fetch menu item to simulate a background fetch for the application. The output window should display something like this:

2014-01-18 21:22:52.861 BGAppRefresh[60167:70b] Background fetch started...
2014-01-18 21:22:53.498 BGAppRefresh[60167:2a0f] {"coord":{"lon":103.85,"lat":1.29},"sys":{"message":0.097,"country":"SG",
"sunrise":1390000400,"sunset":1390043807},"weather":[{"id":803,"main":
"Clouds","description":"broken clouds","icon":"04n"}],"base":
"cmc stations","main":{"temp":299.696,"temp_min":299.696,"temp_max":299.696,
"pressure":1025.97,"sea_level":1026.44,"grnd_level":1025.97,
"humidity":100},"wind":{"speed":8.58,"deg":20.5002},"clouds":{"all":76},
"dt":1390051107,"id":1880252,"name":"Singapore","cod":200}
2014-01-18 21:22:53.503 BGAppRefresh[60167:2a0f] Background fetch completed...

If you double-click on the Home button on the iPhone Simulator to reveal the multitasking bar, you should now see that the UI has been updated (see Figure 7), proving that the system has taken the snapshot of your application.

ios-fetch-7

When you switch back to your application, you will see the weather information that you have obtained during the background fetch (see Figure 8).

ios-fetch-8

Summary

In this article, you have seen the use of the Background Fetch feature that is new in iOS 7. Using this feature, you can schedule regular updates of your application even if your application is in the background. Remember that the system controls the frequency of your background fetch, and that you should always call the completionHandler block after you are done performing the download (regardless of whether you have managed to download new data or not) so that you can be a good citizen on the device.