Episode #92

Background Fetch

14 minutes
Published on October 24, 2013

This video is only available to subscribers. Get access to this video and 572 others.

In this episode we write an application that takes advantage of iOS 7's background fetch feature, allowing us to keep our application updated in the background so that the user doesn't have to wait for updated data when the application is launched.

Episode Links

Turn on Background Fetch

To turn on Background Fetch, go to your project's settings. Under the Capabilities tab, turn on Background modes and select "Background Fetch".

One this is done, you still need to inform the system of how often to fetch in the background. You can do this in your app delegate:

    [application setMinimumBackgroundFetchInterval:UIApplicationBackgroundFetchIntervalMinimum];

You can pass in your own value here, but the system provides two notable special values:

  • UIApplicationBackgroundFetchIntervalMinimum
  • UIApplicationBackgroundFetchIntervalNever

The former is the most frequent setting possible (exact setting not published by Apple) and the latter is how you can disable the feature. This is useful if the user is logged out, has no subscriptions to update, or in our case, has no saved location to check weather for.

It's important to play by the rules here and have this setting closely match how often the data is actually updated.

Responding to Background Updates

- (void)application:(UIApplication *)application performFetchWithCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler {
    NSLog(@"perform Background Fetch...");

    WeatherResult *cachedResult = [[WeatherFetcher sharedInstance] cachedResult];
    if (cachedResult) {
        NSLog(@"background updating weather for %@", cachedResult.location);
        [[WeatherFetcher sharedInstance] fetchWeatherForLocation:cachedResult.location
                                                      completion:^(WeatherResult *result) {
                                                          [[NSNotificationCenter defaultCenter] postNotificationName:@"WeatherUpdated"
                                                                                                              object:result];
                                                          completionHandler(UIBackgroundFetchResultNewData);
                                                      }];
    } else {
        NSLog(@"No location saved, disabling background fetch...");
        [application setMinimumBackgroundFetchInterval:UIApplicationBackgroundFetchIntervalNever];
        completionHandler(UIBackgroundFetchResultNoData);
    }

}

Note that we are passed a completion handler that we must call to indicate to the system that our fetch has completed. The OS will record this time, along with whether or not we said we had new data, to determine if it should modify the background fetch schedule for this app at all.

Also note that in our example, unlike the Apple sample code, doesn't call directly into a view controller to do the fetch. I find an App Delegate directly calling methods on a view controller to be somewhat of a bad design, and I prefer to handle background updates separately from view controller updates.

Fixing the Double Update Problem

We aren't relying on our view controller to do the background fetch but the system still needs to launch our application, load up the visible view controller, and call viewWillAppear: on it so that it can properly snapshot the view (for the app switcher). This causes our app to fetch the data twice. We can check the applicationState property to avoid doing this second fetch.

- (void)loadFromCache {
    WeatherResult *cachedResult = [[WeatherFetcher sharedInstance] cachedResult];

    if (cachedResult) {
        self.searchBar.text = cachedResult.location;
        [self updateTemperature:cachedResult.temperature
                    lastUpdated:cachedResult.updatedAt];

        if ([self needsRefresh:cachedResult.updatedAt] && [[UIApplication sharedApplication] applicationState] == UIApplicationStateActive) {
            [self searchForLocation:cachedResult.location];
        }
    }
}