现在的位置: 首页 > 综合 > 正文

Background Modes in iOS Tutorial

2014年09月05日 ⁄ 综合 ⁄ 共 42250字 ⁄ 字号 评论关闭

Beginning with iOS 4, you can design your apps to stay suspended in memory when users push the home button. Even though the app is in memory, its operations are paused until the user starts it again. Or are they?

Of course there are exceptions to this rule! In certain situations, the app can still run some operations in the background. This tutorial will teach you how and when to use (almost) all of these background operations.

There are important restrictions on the use of background operations. This is not a magic solution for getting “real” multitasking on iOS. Most apps will still get completely suspended when the user switches to another app. Your app is only allowed to keep
running in the background in very specific cases — playing audio, getting location updates, downloading new issues for Newsstand apps, and handling VoIP calls.

If your app does not need to do any of these things, then you’re out of luck… with one exception: all apps get up to 10 minutes to finish whatever they were doing before the app is truly suspended.

So backgrounding may not be for you. But if it is, keep reading!

As you’ll soon learn, there are five basic background modes available to you in iOS. The project you’ll build in this tutorial is a simple tabbed application where each tab demonstrates the backgrounding abilities of one of the modes – from continuously
playing audio to listening for incoming voice-over-IP connections.

Let’s get started!

Getting Started: The Background Modes

Before digging into the project, here’s a quick overview of the five basic background modes available in iOS:

  • Play audio: the app can continue playing and/or recording audio in the background.
  • Receive location updates: the app can continue to get callbacks as the device’s location changes.
  • Perform finite-length tasks: the generic “whatever” case, where the app can run arbitrary code for a limited amount of time.
  • Process Newsstand Kit downloads: specific to Newsstand apps, the app can download content in the background.
  • Provide Voice-over-IP (VoIP) services: the app can run any arbitrary code in the background. Of course, Apple limits its use so that your app must provide VoIP service, too.

This tutorial will cover how to use all five of these modes in the above order, one for each section of the tutorial. If you are only interested in one or several of the modes, feel free to skip ahead!

To get started with the project that will be your tour bus through backgrounding, first download the

sample project
. Here’s some sweet news: the user interface has been pre-configured for you.

background_foreground

You can also follow along using the project’s
GitHub page
. The repository contains steps to create the UI, although this tutorial will get right to the background operations.

Run the sample project and check out your five tabs:

Five Tabs

These tabs are your road map for the rest of the tutorial. First stop… background audio.

Note: For the full effect, you should follow along on a real device. Some background tasks don’t work very well (or at all) on the Simulator.

Playing Audio

There are several ways to play audio on iOS and most of them require implementing callbacks to provide more audio data to play. A callback is when iOS asks your app to do something (like a delegate method), in this case filling up a memory buffer with an
audio waveform.

If you want to play audio from streaming data, you can start a network connection, and the connection callbacks provide continuous audio data.

When you activate the audio background mode, iOS will continue these callbacks even if your app is not the current active app. That’s right – as is the case with four of the five background modes covered in this tutorial, the audio background mode is (almost)
automatic. You just have to activate it and provide the infrastructure to handle it appropriately.

For those of us who are a bit sneaky-minded, you should only use the background audio mode if your app really does play audio to the user. If you try to use this mode just to get CPU time while playing silence in the background, Apple will reject your app!

Who_sneaky

In this section, you’ll add an audio player to your app, turn on the background mode and demonstrate to yourself that it’s working.

To get audio playback, you’ll need to lean on AV Foundation. Open TBFirstViewController.m and add the header import to the top of the file:

#import <AVFoundation/AVFoundation.h>

Now find viewDidLoad and add the following to the end of the method implementation:

    // Set AVAudioSession
    NSError *sessionError = nil;
    [[AVAudioSession sharedInstance] setDelegate:self];
    [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayAndRecord error:&sessionError];
 
    // Change the default output audio route
    UInt32 doChangeDefaultRoute = 1;
    AudioSessionSetProperty(kAudioSessionProperty_OverrideCategoryDefaultToSpeaker,
      sizeof(doChangeDefaultRoute), &doChangeDefaultRoute);

This initializes the audio session and ensures the sound playback goes through the speaker rather than the phone earpiece.

You also need a couple of member variables to keep track of the playback. Insert these two property declarations:

@property (nonatomic, strong) AVQueuePlayer *player;
@property (nonatomic, strong) id timeObserver;

Put them between this code at the top of the file:

@interface TBFirstViewController ()
 
// Insert code here
 
@end

The starter project includes audio files from one of my
favorite royalty-free music websites
: incompetech.com. With credit given, you can use the music for free. So, here you go: all songs are by Kevin MacLeod at incompetech.com. Thanks, Kevin!

One of the easiest ways to play music on iOS is using AV Foundation’s
AVPlayer
. In this tutorial, you’ll use a subclass of AVPlayer called

AVQueuePlayer
. AVQueuePlayer lets you set up a queue of

AVPlayerItems
that will be played automatically, one after the other.

At the end of viewDidLoad again, add this code:

    NSArray *queue = @[
    [AVPlayerItem playerItemWithURL:[[NSBundle mainBundle] URLForResource:@"IronBacon" withExtension:@"mp3"]],
    [AVPlayerItem playerItemWithURL:[[NSBundle mainBundle] URLForResource:@"FeelinGood" withExtension:@"mp3"]],
    [AVPlayerItem playerItemWithURL:[[NSBundle mainBundle] URLForResource:@"WhatYouWant" withExtension:@"mp3"]]];
 
    self.player = [[AVQueuePlayer alloc] initWithItems:queue];
    self.player.actionAtItemEnd = AVPlayerActionAtItemEndAdvance;

This creates an array of AVPlayerItem objects, then creates the
AVQueuePlayer
with this array. In addition, the queue is set up to play the next item when one item ends.

In order to update the song name as the queue progresses, you need to observe the
currentItem property of the player. To do this, add the following to the end of
viewDidLoad:

    [self.player addObserver:self
                  forKeyPath:@"currentItem"
                     options:NSKeyValueObservingOptionNew
                     context:nil];

This makes it so that the class’s observer callback is called whenever the player’s
currentItem property changes. Now you can add the observer method. Put it below
viewDidLoad:

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
    if ([keyPath isEqualToString:@"currentItem"])
    {
        AVPlayerItem *item = ((AVPlayer *)object).currentItem;
        self.lblMusicName.text = ((AVURLAsset*)item.asset).URL.pathComponents.lastObject;
        NSLog(@"New music name: %@", self.lblMusicName.text);
    }
}

When the method gets called, you first make sure the property being updated is the one in which you’re interested. In this case, it’s not actually necessary because there’s only one property being observed, but it’s good practice to check in case you add
more observers later. If it’s the currentItem key, then you use it to update the
lblMusicName label with the file name.

You also need a way to update the label displaying the elapsed time of the currently playing item. The best way to do this is by using the
addPeriodicTimeObserverForInterval:queue:usingBlock: method, which will call the provided block on a given queue.

Add this to the end of viewDidLoad:

    void (^observerBlock)(CMTime time) = ^(CMTime time) {
        NSString *timeString = [NSString stringWithFormat:@"%02.2f", (float)time.value / (float)time.timescale];
        if ([[UIApplication sharedApplication] applicationState] == UIApplicationStateActive) {
            self.lblMusicTime.text = timeString;
        } else {
            NSLog(@"App is backgrounded. Time is: %@", timeString);
        }
    };
 
    self.timeObserver = [self.player addPeriodicTimeObserverForInterval:CMTimeMake(10, 1000)
                                                                  queue:dispatch_get_main_queue()
                                                             usingBlock:observerBlock];

You first create the block that you’ll call whenever the time is updated. If you don’t know how to use blocks (and you should – they’re awesome!), then read the

How to Use Blocks in iOS 5 tutorial
. This block creates a string with the updated time of the song, based on your application’s state.

After that is the call to - (id)addPeriodicTimeObserverForInterval:(CMTime)interval queue:(dispatch_queue_t)queue usingBlock:(void (^)(CMTime time))block to start getting updates.

Let’s pause here and talk a bit about the application’s state.

Your application can have one of five states. Briefly, they are:

  • Not running: Your app is in this state before it starts up.
  • Active: Once your app is started, it becomes active.
  • Inactive: When your app is running but something happens to interrupt it, like a phone call, it becomes inactive. Inactive means that the app is still running in the foreground but it’s not receiving events.
  • Backgrounded: In this state, your app is not in the foreground anymore but it is still able to run code.
  • Suspended: Your app enters this state when it’s no longer able to run code.

If you’d like a more thorough explanation of the differences between these states, the subject is well-documented on Apple’s website at

App States and Multitasking
.

You can check your app’s state by calling [[UIApplication sharedApplication] applicationState]. Keep in mind that you’ll only get three states back:
UIApplicationStateActive, UIApplicationStateInactive, and
UIApplicationStateBackground. Suspended and not running obviously would never happen while your app is running, so there’s no value defined for them.

Let’s return to the code. If the app is in the active state, you need to update the music title label. If not, you just log the update to the console. You could still update the label text while in the background, but the point here is just to demonstrate
that your app continues to receive this callback when your app is in the background.

Now all that’s remaining is for you to add the implementation of didTapPlayPause to get the play/pause button working. Add this new method at the bottom of
TBFirstViewController.m, just above @end:

- (IBAction)didTapPlayPause:(id)sender
{
    self.btnPlayPause.selected = !self.btnPlayPause.selected;
    if (self.btnPlayPause.selected)
    {
        [self.player play];
    }
    else
    {
        [self.player pause];
    }
}

Great, that’s all your code. Build and run, and you should see this:

Screenshot_4_29_13_2_12_PM

Now hit Play and the music will start. Nice!

Let’s test if backgrounding works. Hit the home button (if you’re using the Simulator, hit Cmd-Shift-H). Oops, why did the music stop? Well, there’s still a very crucial piece missing!

For most background modes (the “Whatever” mode is the exception) you need to add a key in
Info.plist to indicate that the app wants to run code while in the background.

Back in Xcode, do the following:

  1. Click the project on the File Navigator;
  2. Click Info;
  3. Click on the (+) that appears when you hover over any line of the list;
  4. Choose Required Background Modes in the list that appears:

When you select this item, XCode will create an array below the item with one blank item. Expand the list on the right and select
App plays audio. In this list, you can see all background modes this tutorial covers – as well as a few more that you can’t use without specialized hardware.

Build and run again. Start the music and hit the home button, and this time you should still hear the music, even though the app is in the background.

If it still doesn’t work for you, it is probably because you’re using the Simulator. Testing it on a device should get it working. You should also see the time updates in your console output in XCode, proof that your code is still working even thought the
app is in the background.

That’s one mode down, if you’re following through the entire tutorial – four to go!

The GitHub repository tag for this point in the tutorial is
BackgroundMusic
.

Receiving Location Updates

When in location background mode, your app will still receive location delegate messages with the updated location of the user, even when the app is in the background. You can control the accuracy of these location updates and even change that accuracy while
backgrounded.

Once again, for the sneaks: you can only use this background mode if your app truly needs this information to provide value for the user. If you use this mode and Apple sees nothing the user will gain from it, your app will be rejected. Sometimes Apple will
also require you to add a warning to your app’s description stating that your app will result in increased battery usage.

The second tab is for location updates, so open TBSecondViewController.m and add some property declarations:

@property (nonatomic, strong) CLLocationManager *locationManager;
@property (nonatomic, strong) NSMutableArray *locations;

The properties should be added to the top of the file between these lines:

@interface TBSecondViewController ()
 
// add code here
 
@end

CLLocationManager is the class you’ll use to get the device’s location updates. You’ll use the
locations array to store the locations so you can plot them on the map.

Now add this to the end of viewDidLoad:

        self.locations = [[NSMutableArray alloc] init];
        self.locationManager = [[CLLocationManager alloc] init];
        self.locationManager.desiredAccuracy = kCLLocationAccuracyBest;
        self.locationManager.delegate = self;

This creates an empty mutable array to store the location updates, then creates the
CLLocationManager object. The code sets the accuracy of the location manager to the most accurate, which you can adjust to whatever your app might need. You’ll learn more about those other accuracy settings and their importance in a moment.

Now you can implement the accuracyChanged: method:

- (IBAction)accuracyChanged:(id)sender
{
    const CLLocationAccuracy accuracyValues[] = {
        kCLLocationAccuracyBestForNavigation,
        kCLLocationAccuracyBest,
        kCLLocationAccuracyNearestTenMeters,
        kCLLocationAccuracyHundredMeters,
        kCLLocationAccuracyKilometer,
        kCLLocationAccuracyThreeKilometers};
 
    self.locationManager.desiredAccuracy = accuracyValues[self.segmentAccuracy.selectedSegmentIndex];
}

accuracyValues is an array with all the possible values of the
desiredAccuracy
property of CLLocationManager. This property controls how accurate you want your location to be.

You might think that this is stupid. Why shouldn’t the location manager always give you the best accuracy possible? Well, there’s a reason for the other values: conservation of battery power. Lower accuracy means less battery used.

So, if you don’t need a lot of accuracy in your app, you should select the value with the least amount of accuracy possible for your purpose. You can change this value whenever you want.

There’s another property that will control how often your app receives location updates, regardless of the value of
desiredAccuracy:
distanceFilter
. This property tells the location manager that you only want to receive a new location update when the device has moved a certain value, in meters.

Again, this value should be as high as it can be to conserve battery power.

Now you can add the code to start getting location updates in the enabledStateChanged: method:

- (IBAction)enabledStateChanged:(id)sender
{
    if (self.switchEnabled.on)
    {
        [self.locationManager startUpdatingLocation];
    }
    else
    {
        [self.locationManager stopUpdatingLocation];
    }
}

The xib file has a UISwitch attached to the switchEnabled outlet that turns location tracking on and off.

Next you need to add the CLLocationManagerDelegate method that will receive location updates. Add this to the end of the file, right before
@end:

#pragma mark - CLLocationManagerDelegate
 
/*
 *  locationManager:didUpdateToLocation:fromLocation:
 *
 *  Discussion:
 *    Invoked when a new location is available. oldLocation may be nil if there is no previous location
 *    available.
 *
 *    This method is deprecated. If locationManager:didUpdateLocations: is
 *    implemented, this method will not be called.
 */
- (void)locationManager:(CLLocationManager *)manager
    didUpdateToLocation:(CLLocation *)newLocation
           fromLocation:(CLLocation *)oldLocation
{
    // Add another annotation to the map.
    MKPointAnnotation *annotation = [[MKPointAnnotation alloc] init];
    annotation.coordinate = newLocation.coordinate;
    [self.map addAnnotation:annotation];
 
    // Also add to our map so we can remove old values later
    [self.locations addObject:annotation];
 
    // Remove values if the array is too big
    while (self.locations.count > 100)
    {
        annotation = [self.locations objectAtIndex:0];
        [self.locations removeObjectAtIndex:0];
 
        // Also remove from the map
        [self.map removeAnnotation:annotation];
    }
 
    if (UIApplication.sharedApplication.applicationState == UIApplicationStateActive)
    {
        // determine the region the points span so we can update our map's zoom.
        double maxLat = -91;
        double minLat =  91;
        double maxLon = -181;
        double minLon =  181;
 
        for (MKPointAnnotation *annotation in self.locations)
        {
            CLLocationCoordinate2D coordinate = annotation.coordinate;
 
            if (coordinate.latitude > maxLat)
                maxLat = coordinate.latitude;
            if (coordinate.latitude < minLat)
                minLat = coordinate.latitude;
 
            if (coordinate.longitude > maxLon)
                maxLon = coordinate.longitude;
            if (coordinate.longitude < minLon)
                minLon = coordinate.longitude;
        }
 
        MKCoordinateRegion region;
        region.span.latitudeDelta  = (maxLat +  90) - (minLat +  90);
        region.span.longitudeDelta = (maxLon + 180) - (minLon + 180);
 
        // the center point is the average of the max and mins
        region.center.latitude  = minLat + region.span.latitudeDelta / 2;
        region.center.longitude = minLon + region.span.longitudeDelta / 2;
 
        // Set the region of the map.
        [self.map setRegion:region animated:YES];
    }
    else
    {
        NSLog(@"App is backgrounded. New location is %@", newLocation);
    }
}

Wow, that’s a big block of code! This isn’t a Core Location tutorial, so let’s not linger over what’s happening here – the explanatory comments should suffice. If the app’s state is active, this code will update the map. If the app is in the background,
you should see the log of location updates in your console output in XCode.

Now that you know about Info.plist, you don’t need to make the same mistake as before! Add another row to the array (“App registers for location updates”) to let iOS know that your app wants to continue receiving location updates while in the background.

Now build and run! Switch to the second tab and flip the switch to ON.

Screenshot_4_29_13_2_20_PM

The first time you do this, you should see the standard dialog asking if the app is allowed to access your location. Tap OK and go for a walk outside or around your building. You should start seeing location updates, even on the Simulator.

After a while, you should see something like this:

Screenshot_4_29_13_2_24_PM

If you background the app, you should see the app updating the location in your console log. Open the app again and you’ll see that the map has all the pins for the locations that were updated while the app was in the background.

If you’re using the Simulator, you can use it to simulate movement, too! Check out the Debug menu:

backgrounding-debug

Try setting the location to Freeway Drive and then hit the home button. You should see the console logs printing out your progress as you simulate that drive down a California highway:

2013-03-07 22:31:11.667 TheBackgrounder[52611:c07] App is backgrounded. New location is <+37.33500926,-122.03272188> +/- 5.00m (speed 7.74 mps / course 246.09) @ 3/7/13, 10:31:11 PM Eastern Daylight Time
2013-03-07 22:31:12.670 TheBackgrounder[52611:c07] App is backgrounded. New location is <+37.33497737,-122.03281282> +/- 5.00m (speed 9.18 mps / course 251.37) @ 3/7/13, 10:31:12 PM Eastern Daylight Time
2013-03-07 22:31:13.669 TheBackgrounder[52611:c07] App is backgrounded. New location is <+37.33494812,-122.03292120> +/- 5.00m (speed 10.78 mps / course 251.72) @ 3/7/13, 10:31:13 PM Eastern Daylight Time
2013-03-07 22:31:14.658 TheBackgrounder[52611:c07] App is backgrounded. New location is <+37.33492222,-122.03304215> +/- 5.00m (speed 12.11 mps / course 254.18) @ 3/7/13, 10:31:14 PM Eastern Daylight Time

The GitHub repository tag for this point in the tutorial is
BackgroundLocation
.

Let’s cruise on to the third tab and the third background mode.

Performing Finite-Length Tasks… or, Whatever

The next background mode is officially called “Executing
a Finite-Length Task in the Background
“. What a mouthful. I think Whatever is catchier!

Technically, this is not a background mode at all, as you don’t have to declare that your app uses this mode in Info.plist. Instead, it’s an API that lets you run arbitrary code for a finite amount of time when your app is in the background. Well… whatever!

You could, for example, use this mode to complete an upload or download. Let’s say you’re building a (yet another) photo-sharing app. If the user takes a picture and leaves the app right away, there might not be time to upload the photo to your servers.
Using this API, you can request additional CPU time to complete the upload, making your users happy and ensuring that one more cat picture has made it to the Internet safely.

Whatever_cheers

But this is just one example. As the code you can run is arbitrary, you can use this API to do pretty much anything: perform lengthy calculations (as in this tutorial), apply filters to images, render a complicated 3D mesh… whatever! Your imagination is
the limit, as long as you keep in mind that you only get some time, not unlimited time.

How much time you get after your app gets backgrounded is determined by iOS. There are no guarantees on the time you’re granted, but you can always check the
backgroundTimeRemaining property of UIApplication. This will tell you how much time you have left.

The general, observation-based consensus is that usually, you get 10 minutes. Again, there are
no guarantees and the API documentation doesn’t even give a ballpark number – so don’t rely on this number. You might get 5 minutes or 5 seconds, so your app needs to be prepared for anything!

Here’s a common task that every CS student should be familiar with: calculating numbers in the
Fibonacci Sequence. The twist here is that you’ll calculate these numbers
in the background!

Open TBThirdViewController.m and add the following properties:

@property (nonatomic, strong) NSDecimalNumber *previous;
@property (nonatomic, strong) NSDecimalNumber *current;
@property (nonatomic) NSUInteger position;
@property (nonatomic, strong) NSTimer *updateTimer;
@property (nonatomic) UIBackgroundTaskIdentifier backgroundTask;

These should be added between these lines at the top of the file:

@interface TBThirdViewController ()
 
// add code here
 
@end

The NSDecimalNumbers will hold the two previous values of the number in the sequence.
NSDecimalNumber is a class that can hold very large numbers, so it’s well suited for your purpose.
position is just a counter that tells you the current number position in the series.

You’ll use updateTimer to demonstrate that even timers continue to work when using this API, and also to slow down the calculations a little so you can observe them.

Find viewDidLoad and add this to the end:

    self.backgroundTask = UIBackgroundTaskInvalid;

Now for the important part, paste in the implementation for didTapPlayPause:

- (IBAction)didTapPlayPause:(id)sender
{
    self.btnPlayPause.selected = !self.btnPlayPause.selected;
    if (self.btnPlayPause.selected)
    {
        self.previous = [NSDecimalNumber one];
        self.current  = [NSDecimalNumber one];
        self.position = 1;
 
        self.updateTimer = [NSTimer scheduledTimerWithTimeInterval:0.5
                                                            target:self
                                                          selector:@selector(calculateNextNumber)
                                                          userInfo:nil
                                                           repeats:YES];
 
        self.backgroundTask = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{
            NSLog(@"Background handler called. Not running background tasks anymore.");
            [[UIApplication sharedApplication] endBackgroundTask:self.backgroundTask];
            self.backgroundTask = UIBackgroundTaskInvalid;
        }];
    }
    else
    {
        [self.updateTimer invalidate];
        self.updateTimer = nil;
        if (self.backgroundTask != UIBackgroundTaskInvalid)
        {
            [[UIApplication sharedApplication] endBackgroundTask:self.backgroundTask];
            self.backgroundTask = UIBackgroundTaskInvalid;
        }
    }
}

Let’s go through this code and see how to use the API.

The button will toggle its selected state depending on whether the calculation is stopped and should start, or is already started and should stop.

First you have to set up the Fibonacci sequence variables. Then you can create an
NSTimer that will fire twice every second and call a method called
calculateNextNumber
(still to come…).

Now comes the essential bit: the call to beginBackgroundTaskWithExpirationHandler:. This is the method that tells iOS that you need more time to complete whatever it is you’re doing in case the app is backgrounded. After this call, if your app
is backgrounded it will still get CPU time until you call endBackgroundTask:.

Well, almost. If you don’t call endBackgroundTask: after a period of time in the background, iOS will call the block defined when you called
beginBackgroundTaskWithExpirationHandler: to give you a chance to stop executing code. So it’s a very good idea to then call
endBackgroundTask: tell the OS that you’re done. If you don’t do this and continue to execute code after this block is called, your app will be terminated!

The second part of the if is simple: it only invalidates the timer and calls
- (void)endBackgroundTask:(UIBackgroundTaskIdentifier)identifier to indicate to iOS that you don’t need any extra CPU time.

It is important that you call endBackgroundTask: for every time you call
beginBackgroundTaskWithExpirationHandler:. If you call beginBackgroundTaskWithExpirationHandler: twice and only call
endBackgroundTask: for one of the tasks, you’re still going to get CPU time until you call
endBackgroundTask: a second time with the value of the second background task. This is also why you needed the
backgroundTask variable.

Now you can implement the little CS project method. Add this to the end of the file, right before
@end:

- (void)calculateNextNumber
{
    NSDecimalNumber *result = [self.current decimalNumberByAdding:self.previous];
 
    if ([result compare:[NSDecimalNumber decimalNumberWithMantissa:1 exponent:40 isNegative:NO]] == NSOrderedAscending)
    {
        self.previous = self.current;
        self.current  = result;
        self.position++;
    }
    else
    {
        // This is just too much.... Let's start over.
        self.previous = [NSDecimalNumber one];
        self.current  = [NSDecimalNumber one];
        self.position = 1;
    }
 
    NSString *currentResultLabel = [NSString stringWithFormat:@"Position %d = %@", self.position, self.current];
    if (UIApplication.sharedApplication.applicationState == UIApplicationStateActive)
    {
        self.txtResult.text = currentResultLabel;
    }
    else
    {
        NSLog(@"App is backgrounded. Next number = %@", currentResultLabel);
        NSLog(@"Background time remaining = %.1f seconds", [UIApplication sharedApplication].backgroundTimeRemaining);
    }
}

Once again, here’s the application state trick to show the result even you’re your app’s in the background. In this case, there’s one more interesting piece of information: the value of the
backgroundTimeRemaining property. The calculation will only stop when iOS calls the block added through the call to
beginBackgroundTaskWithExpirationHandler:.

Build and run, then switch to the third tab.

Screenshot_4_29_13_2_26_PM

Tap Start and you should see the app calculating the values. Now hit the home button and watch the output in XCode’s console. You should see the app still updating the numbers while the time remaining goes down.

In most cases, this time will start with 600 (600 seconds = 10 minutes) and go down to 5 seconds. If you wait for the time to expire when you reach 5 seconds (could be another value depending on your specific conditions), the expiration block will be invoked
and your app should stop outputting anything. Then if you go back to the app, the timer should start firing again and the whole madness will continue.

There’s only one bug in this code, and it gives me the opportunity to explain about the background notifications. Suppose you background the app and wait until the allotted time expires. In this case, your app will call the expiration handler and invoke
endBackgroundTask:, thus ending the need for background time.

If you then return to the app, the timer will continue to fire. But if you leave the app again, you’ll get no background time at all. Why? Because nowhere between the expiration and returning to the background did the app call
beginBackgroundTaskWithExpirationHandler:.

How can you solve this? There are a number of ways to go about it and one of them is to use a state change notification.

There are two ways you can get a notification that your app has changed state. The first is through your main app delegate’s methods. The second is by listening for some notifications that iOS sends to your app:

  • UIApplicationWillResignActiveNotification and applicationWillResignActive: These are sent and called when your app is about to enter the inactive state. At this point, your app is not yet in the background – it’s still the foreground app
    but it won’t receive any UI events.
  • UIApplicationDidEnterBackgroundNotification and applicationDidEnterBackground: These are sent and called when the app enters the background state. At this point, your app is not active anymore and you’re just getting this last chance to
    run some code. This is the perfect moment to call beginBackgroundTaskWithExpirationHandler: if you want to get more CPU time.
  • UIApplicationWillEnterForegroundNotification and applicationWillEnterForeground: These are sent and called when the app is returning to the active state. The app is still in the background but you can already restart anything you want
    to do. This is a good time to call endBackgroundTask: if you’re only calling
    beginBackgroundTaskWithExpirationHandler: when actually entering the background.
  • UIApplicationDidBecomeActiveNotification and applicationDidBecomeActive: These are sent and called right after the previous notification, in case your app is returning from the background. These are also sent and called if your app was
    only temporarily interrupted – by a call, for example – and never really entered the background, but you received the
    UIApplicationWillResignActiveNotification notification.

You can see all this in graphic detail (literally – there are some nice flow charts) in Apple’s documentation for

App States and Multitasking
.

The next section will cover how to use these notifications. In true CS education fashion, fixing the
beginBackgroundTaskWithExpirationHandler bug will be left as an exercise for the reader. ;]

The GitHub repository tag for this point in the tutorial is
BackgroundWhatever
.

Processing Newsstand Kit Downloads

In iOS 5, Apple introduced the Newsstand API. It allows you to build magazine and newspaper apps that have some special characteristics. Apps built with the Newstand API don’t have the usual app icon since they’re installed inside the Newsstand app.

Newsstand mode is very specific to Newsstand apps. It provides a set of APIs that make it easy for your app to start a very large file download (as some magazine downloads tend to be) even if your app is not active (using push notifications) and keep going
until the whole file has been downloaded.

You can’t use this mode without declaring that your app is a Newsstand app, or the code will not work as intended. It’s not even a matter of Apple rejecting you in this case – the code will just not work!

Note: If you want the whole scoop on Newsstand,
iOS 5 by Tutorials
has a great chapter on the subject.

The sample project xib has a UITextField with a URL to be loaded in a
UIWebView. By default, the URL is set to https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/ProgrammingWithObjectiveC/ProgrammingWithObjectiveC.pdf, which is a big PDF file, to help demonstrate that the download continues after
your app’s been backgrounded. If you know of any other big PDF file, you can use that instead.

Start off in the usual way, by adding two properties to the top of TBFourthViewController.m:

@property (nonatomic, strong) NKIssue *currentIssue;
@property (nonatomic, strong) NSString *issueFilename;

The first thing to do is implement the method in UITextFieldDelegate that gets called when the user presses the return key after editing the field. Add this code anywhere inside the class:

#pragma mark - UITextFieldDelegate
 
- (BOOL)textFieldShouldReturn:(UITextField *)textField
{
    self.webView.hidden = YES;
    self.progress.progress = 0.0f;
    self.progress.hidden = NO;
 
    NKLibrary *library = [NKLibrary sharedLibrary];
    for (NKIssue *issue in [library.issues copy])
    {
        [library removeIssue:issue];
    }
    self.currentIssue = [library addIssueWithName:@"test" date:[NSDate date]];
 
    NSURL *downloadURL = [[NSURL alloc] initWithString:self.txtURL.text];
    NSURLRequest *request = [NSURLRequest requestWithURL:downloadURL];
    NKAssetDownload *assetDownload = [self.currentIssue addAssetWithRequest:request];
    [assetDownload downloadWithDelegate:self];
    [textField resignFirstResponder];
    return YES;
}

First you hide the web view and show the progress view instead, with its progress set to zero.
NKLibrary offers a singleton object that manages magazine/newspaper issues in your app. You’ll need to use it to create a
NKIssue object.

You first remove every issue to clean up and to avoid an error when adding an issue with the same name. Next, you create an issue, giving it any name. In a real situation, this would be the issue name or number, as every issue in a library needs to have
a unique name.

You then create an NSURL with the value in the text field, create the
NSURLRequest from that and finally, add the request to the NKIssue object. With this new
NKAssetDownload object, you can start downloading the file, setting the view controller class as a delegate.

You can set up an optional delegate method for NSURLConnection that will send updates as the download progresses. Add this code anywhere inside the class:

#pragma mark - NSURLConnectionDownloadDelegate
 
- (void)connection:(NSURLConnection *)connection
      didWriteData:(long long)bytesWritten
 totalBytesWritten:(long long)totalBytesWritten
expectedTotalBytes:(long long)expectedTotalBytes
{
    float progress = (float)totalBytesWritten / (float)expectedTotalBytes;
    if (UIApplication.sharedApplication.applicationState == UIApplicationStateActive)
    {
        self.progress.progress = progress;
    }
    else
    {
        NSLog(@"App is backgrounded. Progress = %.1f", progress);
    }
}
 
- (void)connectionDidFinishDownloading:(NSURLConnection *)connection
                        destinationURL:(NSURL *)destinationURL
{
    self.issueFilename = destinationURL.pathComponents.lastObject;
    NSURL *fileURL = [self.currentIssue.contentURL URLByAppendingPathComponent:self.issueFilename];
 
    [[NSFileManager defaultManager] moveItemAtURL:destinationURL
                                            toURL:fileURL
                                            error:nil];
 
    if (UIApplication.sharedApplication.applicationState == UIApplicationStateActive)
    {
        self.webView.hidden = NO;
        self.progress.hidden = YES;
        NSURL *fileURL = [self.currentIssue.contentURL URLByAppendingPathComponent:self.issueFilename];
        [self.webView loadRequest:[NSURLRequest requestWithURL:fileURL]];
    }
    else
    {
        NSLog(@"App is backgrounded. Download finished");
    }
}

The first method gets called when there’s some new data available in the download. You don’t have to do much, as the OS is taking care of all the data, but you can update the UI in case the app is active or output the progress to the console if the app is
in the background.

The second method gets called when the download is done. The downloaded file has to be moved to the location where the Newsstand API expects it to be. This allows the API to remove old issues if your device gets full.

After the file has been moved, you can update the UI if the app is active, and log to the console otherwise.

But what happens if the download finishes in the background? The web view will never be updated with the PDF! You can fix this using one of the notifications mentioned in the previous section.

First extract the update code from the method above so you can use it again without having to repeat code (remember to keep your code
DRY all the time). Add this method anywhere in the class:

- (void)updateWebView
{
    self.webView.hidden = NO;
    self.progress.hidden = YES;
    NSURL *fileURL = [self.currentIssue.contentURL URLByAppendingPathComponent:self.issueFilename];
    [self.webView loadRequest:[NSURLRequest requestWithURL:fileURL]];
}

Then remove the code from connectionDidFinishDownloading:destinationURL: so that the
if statement becomes:

    if (UIApplication.sharedApplication.applicationState == UIApplicationStateActive)
    {
        [self updateWebView];
    }
    else
    {
        NSLog(@"App is backgrounded. Download finished");
    }

Now add the method you want to get called when the app becomes active:

- (void)appBecameActive
{
    if (self.currentIssue && self.currentIssue.downloadingAssets.count == 0 && self.webView.hidden)
    {
        [self updateWebView];
    }
}

And finally, add this code to the end of viewDidLoad:

    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(appBecameActive)
                                                 name:UIApplicationDidBecomeActiveNotification
                                               object:nil];

Don’t forget that whenever you add an observer, you need to remove it before the object gets destroyed. So let’s implement
dealloc:

- (void)dealloc
{
   [[NSNotificationCenter defaultCenter] removeObserver:self];
}

Now you’re almost done, but guess what’s missing… the Info.plist keys! This time you need to add a second key to set the app as a Newsstand app.

First add a new key to the Required background modes array:

Then add a new key to the main list of keys and select Application presents content in Newsstand:

After you select this item, the line will change to a Boolean. Make sure to change the value to
Yes.

Now build and run, then switch to the fourth tab. This is what you should see:

Screenshot_4_29_13_2_28_PM

Select the text field and tap the return key on the keyboard. The file should begin downloading and you should see the progress bar go from zero to full. After the download is finished, you should see the file in the web view:

Screenshot_4_29_13_2_30_PM

All right, it works in the foreground! But will it pass the true test?

Stop the app and run it again. This time, hit the home button right after tapping return and you should see updates in your console. Go back to the app and you should see the web view update with the PDF content. Hooray!

If you have a fast Internet connection, then the download might finish too quickly, before you had a chance to see anything happen in the console. In that case, run the app on your device and use your cell data to slow things down. Or try to find a larger
PDF file to download.

One thing you’ll notice is that your app doesn’t have a normal icon anymore. It should be inside the Newsstand app with a default newspaper icon:

The GitHub repository tag for this point in the tutorial is
BackgroundNewsstand
.

Providing VoIP Services

This last one is a very powerful background mode, as it allows your app to run arbitrary code while in the background. This mode is better than the “Whatever” API because you can run the code for an indefinite amount of time. Better yet, if the app crashes
or the user reboots the phone, the app is started automatically in the background. Very nice!

The catch is that, again, your app needs to provide the user with some sort of VoIP functionality or, you guessed it, Apple will reject you. Well, not
you, of course, but your app.

worthwhileperson

Building a VoIP app is way beyond the scope of this tutorial, but I’ll try to illustrate the principles at least. VoIP, of course, stands for Voice-over-IP or internet telephony. For the tutorial, you’ll build a simple app that connects to a server, keeps
the connection open while in the background and gets callbacks when the app receives something through that connection.

To begin, open TBFifthViewController.m and add these property declarations to the top:

@property (nonatomic, strong) NSInputStream *inputStream;
@property (nonatomic, strong) NSOutputStream *outputStream;
@property (nonatomic, strong) NSMutableString *communicationLog;
@property (nonatomic) BOOL sentPing;

Also define a few string constants that you’ll use as commands in your connection. Add this code right before
@implementation TBFifthViewController:

const uint8_t pingString[] = "ping\n";
const uint8_t pongString[] = "pong\n";

To start the implementation, here’s a convenience method that adds events to the text view. You’ll use this extensively to indicate when the connection is completed, if it’s broken or when something is received. Add this code anywhere in your class:

- (void)addEvent:(NSString *)event
{
    [self.communicationLog appendFormat:@"%@\n", event];
    if (UIApplication.sharedApplication.applicationState == UIApplicationStateActive)
    {
        self.txtReceivedData.text = self.communicationLog;
    }
    else
    {
        NSLog(@"App is backgrounded. New event: %@", event);
    }
}

Simple enough. It appends to the string and updates the UI if the app is active, or just logs to the console if in the background.

Now add the implementation for didTapConnect:, which gets called when the user taps the Connect button to start things off:

- (IBAction)didTapConnect:(id)sender
{
    if (!self.inputStream)
    {
        // 1
        CFReadStreamRef readStream;
        CFWriteStreamRef writeStream;
        CFStreamCreatePairWithSocketToHost(NULL, (__bridge CFStringRef)(self.txtIP.text), [self.txtPort.text intValue], &readStream, &writeStream);
 
        // 2
        self.sentPing = NO;
        self.communicationLog = [[NSMutableString alloc] init];
        self.inputStream = (__bridge_transfer NSInputStream *)readStream;
        self.outputStream = (__bridge_transfer NSOutputStream *)writeStream;
        [self.inputStream setProperty:NSStreamNetworkServiceTypeVoIP forKey:NSStreamNetworkServiceType];
 
        // 3
        [self.inputStream setDelegate:self];
        [self.outputStream setDelegate:self];
        [self.inputStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
        [self.outputStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
 
        // 4
        [self.inputStream open];
        [self.outputStream open];
 
        // 5
        [[UIApplication sharedApplication] setKeepAliveTimeout:600 handler:^{
            if (self.outputStream)
            {
                [self.outputStream write:pingString maxLength:strlen((char*)pingString)];
                [self addEvent:@"Ping sent"];
            }
        }];
    }
}

This looks a little complicated because you’re creating a two-way stream.

  1. The easiest way to do so is by using the CFStreamCreatePairWithSocketToHost function. That means a lot of bridging to be able to use the
    NSInputStream and NSOutputStream classes afterwards.
  2. After you create the stream pair with CFStreamCreatePairWithSocketToHost, you bridge them to the appropriate Objective-C classes. The
    setProperty:forKey: method call is really important here, as it indicates to the OS that this connection should be maintained even when the app is not the foreground app. You only need to do this for the input stream.
  3. Next, you set the controller object as the delegate and you set the run loop to be the main run loop for both streams. The OS needs to know in which run loop it should call the delegate methods, and the best run loop in this case is the one associated with
    the app’s main thread. You’ll want to update the UI when you get a message and you need to be in the main run loop for that.
  4. After this, the streams are configured and you only need to call open on both.
  5. The final thing you do in the code above is specific to VoIP apps. With the call to
    setKeepAliveTimeout:handler: you can set a handler that will be called periodically while the app is in the background. This allows your app to do anything, but it should be used to send a “ping” to your server to keep the connection alive. You’ve
    configured it to be called every 10 minutes – the minimum value allowed for this method, according to the documentation. All you’re doing here is sending a ping command to the server and logging the event.

Now you need to add the stream delegate that will receive updates for the connection. Add this code anywhere in your class:

#pragma mark - NSStreamDelegate
 
- (void)stream:(NSStream *)aStream handleEvent:(NSStreamEvent)eventCode
{
    switch (eventCode) {
        case NSStreamEventNone:
            // do nothing.
            break;
 
        case NSStreamEventEndEncountered:
            [self addEvent:@"Connection Closed"];
            break;
 
        case NSStreamEventErrorOccurred:
            [self addEvent:[NSString stringWithFormat:@"Had error: %@", aStream.streamError]];
            break;
 
        case NSStreamEventHasBytesAvailable:
            if (aStream == self.inputStream)
            {
                uint8_t buffer[1024];
                NSInteger bytesRead = [self.inputStream read:buffer maxLength:1024];
                NSString *stringRead = [[NSString alloc] initWithBytes:buffer length:bytesRead encoding:NSUTF8StringEncoding];
                stringRead = [stringRead stringByTrimmingCharactersInSet:[NSCharacterSet newlineCharacterSet]];
 
                [self addEvent:[NSString stringWithFormat:@"Received: %@", stringRead]];
 
                if ([stringRead isEqualToString:@"notify"])
                {
                    UILocalNotification *notification = [[UILocalNotification alloc] init];
                    notification.alertBody = @"New VOIP call";
                    notification.alertAction = @"Answer";
                    [[UIApplication sharedApplication] presentLocalNotificationNow:notification];
                }
                else if ([stringRead isEqualToString:@"ping"])
                {
                    [self.outputStream write:pongString maxLength:strlen((char*)pongString)];
                }
            }
            break;
 
        case NSStreamEventHasSpaceAvailable:
            if (aStream == self.outputStream && !self.sentPing)
            {
                self.sentPing = YES;
                if (aStream == self.outputStream)
                {
                    [self.outputStream write:pingString maxLength:strlen((char*)pingString)];
                    [self addEvent:@"Ping sent"];
                }
            }
            break;
 
        case NSStreamEventOpenCompleted:
            if (aStream == self.inputStream)
            {
                [self addEvent:@"Connection Opened"];
            }
            break;
 
        default:
            break;
    }
}

This method takes care of every possible event that might happen with the connection. Most of them are simple and self-explanatory.

The NSStreamEventHasBytesAvailable condition usually occurs in the input stream, but it should be checked anyway. You read the buffer into a string, trim the newline and then log the whole thing.

Then comes the fun part: if the event is “notify”, you schedule a local notification. In a real VoIP app, this would correspond to an incoming call so that the notification gets shown even if the app is running in the background.

If the command is “ping”, you send a “pong” response to the server.

You call NSStreamEventHasSpaceAvailable when there’s space in the output buffer to send data. When you get this for the first time and only for the first time, you send a ping command to the server.

To get this running, of course you need to add the App provides Voice over IP services background mode to Info.plist:

Before you run the app, you need a server with which to test it. You can use a handy little utility every Mac already has called
netcat that, among other things, allows you to easily create simple text-based servers. Open a Terminal session (from the
Applications/Utilities/Terminal.app app) and type this command:

nc -l 10000

This starts a server on port 10000 and starts listening for a connection. Now go back to Xcode and run the app:

Screenshot_4_29_13_2_32_PM

If you’re running on the Simulator, you can leave the address as 127.0.0.1. If you’re testing on a device, you might need to find out the address of your Mac on your network and change the value of the
Address field to your Mac’s IP.

Now click Connect. You should see a ping come up in your console when the connection is established.

In your Terminal console, type ping and hit return. You should see a pong being sent back from the app. If you send something other than “ping”, for example
xxxxx, then the app does not send a response.

Also try the notify command. You’ll need to run on a device, as notifications don’t work on the Simulator.

If you hit the home button and send a ping command on the Terminal, you should still see a pong response. And if running on the device, trying sending it the notify command when the app is in the background and you should see the notification:

That’s the fifth, most powerful background mode calling. Use it wisely!

The GitHub repository tag for this point in the tutorial is
BackgroundVoIP
.

Where to Go From Here?

You can
download the full project
with all of the source code from the above tutorial here.

If you want to read Apple’s documentation on what we covered here, the best place to start is in

Background Execution and Multitasking
. This documentation explains every background mode and has links to the appropriate section of the documentation for every mode.

A particularly interesting section of this document is the one that talks about

being a responsible background app
. There are some details that might or might not relate to your app here that you should know before releasing an app that runs in the background.

I hope you enjoyed the tutorial. You’re now ready to go off on your own and create wonderful things with whichever of the background modes are useful for your purposes.

Remember the whole project is in
my GitHub
so download it, fork it, fix any bugs and have fun!

转自:http://www.raywenderlich.com/29948/backgrounding-for-ios

抱歉!评论已关闭.