Update note: This tutorial was updated to iOS 8 and Swift by Ray Fix. Original post by Gustavo Ambrozio.
Since the ancient days of 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 specific situations, the app can still run some operations in the background. This tutorial will teach you how and when to use some of the most common background operations.
There are important restrictions on the use of background operations and the specifics have evolved with each release of iOS in an effort to improve user experience and extend battery life. Background modes are 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. For example, these include playing audio, getting location updates, or fetching the latest content from a server.
Previous to iOS 7, apps were given up to 10 continuous minutes to finish whatever they were doing before being truly suspended. With the advent of NSURLSession
there is a much more elegant way to handle large network transfers. Because of this, the available background time for arbitrary computation has been reduced to just a few minutes and is no longer necessarily continuous.
So backgrounding may not be for you. But if it is, keep reading!
As you’ll soon learn, there are several background modes available to you in iOS. The project you’ll build in this tutorial is a simple tabbed application that explores four of the most common modes – from continuously playing audio to periodically fetching updated content.
Getting Started
Before digging into the project, here’s a quick overview of the basic background modes available in iOS. In Xcode 6, you can see the list by pulling up the Capabilities tab of your app target. It looks like this:
To get to the background modes capability list you (1) select the project from the Project Navigator, (2) select the app target, (3) select the Capabilities tab, and (4) turn the Background Modes switch on.
In this tutorial you will investigate four ways of doing background processing.
- 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.
- Background Fetch: Get updates to the latest content scheduled by iOS.
This tutorial will cover how to use these four 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 starter project. Here’s some sweet news: the user interface has been pre-configured for you.
Run the sample project and check out your four 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. In my experience, if you forget a configuration setting, the app might appear to run fine in the background on the Simulator. However, when you switch to a real device, it doesn’t work at all.
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 – 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!
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 AudioViewController.swift and add the import to the top of the file right after import UIKit
:
import AVFoundation |
Override viewDidLoad()
with the following implementation:
override func viewDidLoad() { super.viewDidLoad() var error: NSError? var success = AVAudioSession.sharedInstance().setCategory( AVAudioSessionCategoryPlayAndRecord, withOptions: .DefaultToSpeaker, error: &error) if !success { NSLog("Failed to set audio session category. Error: \(error)") } } |
This uses the audio session singleton sharedInstance()
to set the category of playback and ensure sound goes through the speaker rather than the phone earpiece. It checks whether the call fails and logs the error if it ever does. An actual app might want to present an error dialog in response to a failure, but no need to get bogged down in details here.
Next you want to add a player
property method to AudioViewController
:
var player: AVQueuePlayer! |
This is an implicitly unwrapped optional so it starts off as nil
and you will initialize it in viewDidLoad()
.
The starter project includes audio files from incompetech.com, a favorite royalty-free music website. 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!
Going back to viewDidLoad()
add the following to the end of the method:
let songNames = ["FeelinGood", "IronBacon", "WhatYouWant"] let songs = songNames.map { AVPlayerItem(URL: NSBundle.mainBundle().URLForResource($0, withExtension: "mp3")) } player = AVQueuePlayer(items: songs) player.actionAtItemEnd = .Advance |
This takes the list of songs and maps them to URLs in the main bundle and turns them into AVPlayerItem
s that can then be put into a AVQueuePlayer
. 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 currentItem
of the player. To do this, add the following to the end of viewDidLoad()
player.addObserver(self, forKeyPath: "currentItem", options: .New | .Initial , context: nil) |
This makes it so that the class’s observer callback is called on initialization and whenever the player’s currentItem
changes.
Now you can add the observer method. Put it below viewDidLoad()
.
override func observeValueForKeyPath(keyPath: String, ofObject object: AnyObject, change: [NSObject : AnyObject], context: UnsafeMutablePointer<Void>) { if keyPath == "currentItem", let player = object as? AVPlayer, currentItem = player.currentItem?.asset as? AVURLAsset { songLabel.text = currentItem.URL?.lastPathComponent ?? "Unknown" } } |
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 songLabel
with the file name. If for some reason the current item’s URL is not available, it falls back to the string “Unknown”.
You also need a way to update timeLabel
to display the elapsed time of the currently playing item. The best way to do this is by using addPeriodicTimeObserverForInterval(_:queue:usingBlock:)
, which will call the provided block on a given queue. Add this to the end of viewDidLoad
:
player.addPeriodicTimeObserverForInterval(CMTimeMake(1, 100), queue: dispatch_get_main_queue()) { [unowned self] time in let timeString = String(format: "%02.2f", CMTimeGetSeconds(time)) if UIApplication.sharedApplication().applicationState == .Active { self.timeLabel.text = timeString } else { println("Background: \(timeString)") } } |
This adds a periodic observer to the player that gets called back every 1/100 of a second and updates the UI to the time if the app is in the foreground.
Important: Since you are going to update the UI in the closure, you must make sure the code is called on the main queue. This is why you specify dispatch_get_main_queue()
for the queue parameter.
Pause here and consider 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 Execution States for Apps.
You can check your app’s state by reading UIApplication.sharedApplication().applicationState
. Keep in mind that you’ll only get three states back: .Active
, .Inactive
, and .Background
. Suspended and not running obviously would never happen while your app is running code.
Returning to the code, if the app is in the active state, you need to update the music title label. 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 playPauseAction(_:)
to get the play/pause button working. Update the empty implementation of playPauseAction(_:)
in AudioViewController
with:
@IBAction func playPauseAction(sender: UIButton) { sender.selected = !sender.selected if sender.selected { player.play() } else { player.pause() } } |
Great, that’s all your code. Build and run, and you should see this:
Now hit Play and the music will start. Nice!
Test if backgrounding works. Hit the home button (if you’re using the Simulator, hit Cmd-Shift-H). If you are running on an actual device (and not the Simulator in Xcode 6.1) the music will stop. Why? 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. Fortunately, Xcode 6 makes this a matter of clicking on a checkbox.
Back in Xcode, do the following:
- Click the project on the Project Navigator;
- Click TheBackgrounder target;
- Click on the Capabilities tab;
- Scroll down to Background Modes and turn the switch on;
- Check Audio and AirPlay.
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.
You should also see the time updates in your Console output in Xcode, proof that your code is still working even though the app is in the background. You can download the partially finished sample project up to this point.
That’s one mode down, if you’re following through the entire tutorial – three to go!
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 LocationViewController.swift and add some properties with inline initialization to LocationViewController
.
var locations = [MKPointAnnotation]() lazy var locationManager: CLLocationManager! = { let manager = CLLocationManager() manager.desiredAccuracy = kCLLocationAccuracyBest manager.delegate = self manager.requestAlwaysAuthorization() return manager }() |
You’ll use locations
to store the locations so you can plot them on the map. CLLocationManager
is what you’ll use to get the device’s location updates. You instantiate it in a lazy
way so the first time the property is accessed the function is called and it is initialized.
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. Notice that you also are calling requestAlwaysAuthorization()
. This is a requirement in iOS 8, and brings up a user interface asking for permission to use location in the background.
Now you can fill in the empty implementation of accuracyChanged(_:)
in LocationViewController
:
@IBAction func accuracyChanged(sender: UISegmentedControl) { let accuracyValues = [ kCLLocationAccuracyBestForNavigation, kCLLocationAccuracyBest, kCLLocationAccuracyNearestTenMeters, kCLLocationAccuracyHundredMeters, kCLLocationAccuracyKilometer, kCLLocationAccuracyThreeKilometers] locationManager.desiredAccuracy = accuracyValues[sender.selectedSegmentIndex]; } |
accuracyValues
is an array with all the possible values of desiredAccuracy
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.
That means you should select the value for the least amount of accuracy that you can live with for your app. 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 enabledChanged(_:)
:
@IBAction func enabledChanged(sender: UISwitch) { if sender.on { locationManager.startUpdatingLocation() } else { locationManager.stopUpdatingLocation() } } |
The storyboard has a UISwitch
attached to this action that turns location tracking on and off.
Next you need to add a CLLocationManagerDelegate
method to receive location updates. Add the following method to LocationViewController
.
// MARK: - CLLocationManagerDelegate func locationManager(manager: CLLocationManager!, didUpdateToLocation newLocation: CLLocation!, fromLocation oldLocation: CLLocation!) { // Add another annotation to the map. let annotation = MKPointAnnotation() annotation.coordinate = newLocation.coordinate // Also add to our map so we can remove old values later locations.append(annotation) // Remove values if the array is too big while locations.count > 100 { let annotationToRemove = locations.first! locations.removeAtIndex(0) // Also remove from the map mapView.removeAnnotation(annotationToRemove) } if UIApplication.sharedApplication().applicationState == .Active { mapView.showAnnotations(locations, animated: true) } else { NSLog("App is backgrounded. New location is %@", newLocation) } } |
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 Background Modes, you don’t need to make the same mistake as before! Check the box for Location updates to let iOS know that your app wants to continue receiving location updates while in the background.
In addition to checking this box, iOS 8 also requires that you set a key in your Info.plist allows you to explain to the user why background updates are needed. If you do not include this, location will silently fail.
- Select the Info.plist file.
- Click on the + button to add a key.
- Add the key named NSLocationAlwaysUsageDescription.
- Write a convincing description of why you need location in the background.
Now build and run! Switch to the second tab and flip the switch to ON.
When you do this the first time, you will see the message that you put into your Info.plist. Tap Allow 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:
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 \ Location menu:
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:
2014-12-21 20:05:13.334 TheBackgrounder[21591:674586] App is backgrounded. New location is <+37.33482105,-122.03350886> +/- 5.00m (speed 15.90 mps / course 255.94) @ 12/21/14, 8:05:13 PM Pacific Standard Time 2014-12-21 20:05:14.813 TheBackgrounder[21591:674586] App is backgrounded. New location is <+37.33477977,-122.03369603> +/- 5.00m (speed 17.21 mps / course 255.59) @ 12/21/14, 8:05:14 PM Pacific Standard Time 2014-12-21 20:05:15.320 TheBackgrounder[21591:674586] App is backgrounded. New location is <+37.33474691,-122.03389325> +/- 5.00m (speed 18.27 mps / course 257.34) @ 12/21/14, 8:05:15 PM Pacific Standard Time 2014-12-21 20:05:16.330 TheBackgrounder[21591:674586] App is backgrounded. New location is <+37.33470894,-122.03411085> +/- 5.00m (speed 19.27 mps / course 257.70) @ 12/21/14, 8:05:16 PM Pacific Standard Time |
You can download the partially finished sample project up to this point. 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 (or use the Background Mode checkboxes). 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!
In the past, this mode was often used to complete an upload or download and a generous (but not guaranteed) 10 minutes was provided to accomplish these tasks. But what if the connection was slow and the process still didn’t finish? It would leave your app in an odd state and you would have to add lots of error handling code to make things work sanely. Because of this, Apple introduced NSURLSession
.
Although not a topic of this tutorial, NSURLSession
is robust in the face of backgrounding and even device reboots and accomplishes this in a power efficient way. If you are dealing with large downloads, check out our NSURLSession tutorial.
A still very valid use case of the Whatever background mode is to complete some long running task, such as rendering and writing a video to the camera roll.
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 backgroundTimeRemaining
on UIApplication
. This will tell you how much time you have left.
The general, observation-based consensus is that you get 3 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 WhateverViewController.swift and add the following properties to WhateverViewController
:
var previous = NSDecimalNumber.one() var current = NSDecimalNumber.one() var position: UInt = 1 var updateTimer: NSTimer? var backgroundTask: UIBackgroundTaskIdentifier = UIBackgroundTaskInvalid |
The NSDecimalNumbers
will hold the two previous values of the number in the sequence. NSDecimalNumber
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.
Add some utility methods to WhateverViewController
that reset the Fibonacci calculation and that start and stop a background-able task:
func resetCalculation() { previous = NSDecimalNumber.one() current = NSDecimalNumber.one() position = 1 } func registerBackgroundTask() { backgroundTask = UIApplication.sharedApplication().beginBackgroundTaskWithExpirationHandler { [unowned self] in self.endBackgroundTask() } assert(backgroundTask != UIBackgroundTaskInvalid) } func endBackgroundTask() { NSLog("Background task ended.") UIApplication.sharedApplication().endBackgroundTask(backgroundTask) backgroundTask = UIBackgroundTaskInvalid } |
Now for the important part, fill in the empty implementation for didTapPlayPause(_:)
@IBAction func didTapPlayPause(sender: UIButton) { sender.selected = !sender.selected if sender.selected { resetCalculation() updateTimer = NSTimer.scheduledTimerWithTimeInterval(0.5, target: self, selector: "calculateNextNumber", userInfo: nil, repeats: true) registerBackgroundTask() } else { updateTimer?.invalidate() updateTimer = nil if backgroundTask != UIBackgroundTaskInvalid { endBackgroundTask() } } } |
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 calculateNextNumber()
(still to come).
Now comes the essential bit: the call to registerBackgroundTask()
that in turn calls 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 closure 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()
to 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 endBackgroundTask()
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 backgroundTask
.
Now you can implement the little CS project method. Add the following method to WhateverViewController
:
func calculateNextNumber() { let result = current.decimalNumberByAdding(previous) let bigNumber = NSDecimalNumber(mantissa: 1, exponent: 40, isNegative: false) if result.compare(bigNumber) == .OrderedAscending { previous = current current = result ++position } else { // This is just too much.... Start over. resetCalculation() } let resultsMessage = "Position \(position) = \(current)" switch UIApplication.sharedApplication().applicationState { case .Active: resultsLabel.text = resultsMessage case .Background: NSLog("App is backgrounded. Next number = %@", resultsMessage) NSLog("Background time remaining = %.1f seconds", UIApplication.sharedApplication().backgroundTimeRemaining) case .Inactive: break } } |
Once again, here’s the application state trick to show the result even when your app’s in the background. In this case, there’s one more interesting piece of information: the value of backgroundTimeRemaining
. 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.
Tap Play 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 180 (180 seconds = 3 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
andapplicationWillResignActive(_:)
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
andapplicationDidEnterBackground(_:)
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 callbeginBackgroundTaskWithExpirationHandler(_:)
if you want to get more CPU time.UIApplicationWillEnterForegroundNotification
andapplicationWillEnterForeground(_:)
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 callendBackgroundTask()
if you’re only callingbeginBackgroundTaskWithExpirationHandler(_:)
when actually entering the background.UIApplicationDidBecomeActiveNotification
andapplicationDidBecomeActive(_:)
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 receivedUIApplicationWillResignActiveNotification
.
You can see all this in graphic detail (literally – there are some nice flow charts) in Apple’s documentation for App States for Apps.
Time to fix the bug. First, override viewDidLoad()
and subscribe to UIApplicationDidBecomeActiveNotification
.
override func viewDidLoad() { super.viewDidLoad() NSNotificationCenter.defaultCenter().addObserver(self, selector: Selector("reinstateBackgroundTask"), name: UIApplicationDidBecomeActiveNotification, object: nil) } |
This designates the selector reinstateBackgroundTask
to be called whenever the application becomes active.
Whenever you subscribe to a notification you should also think about where to unsubscribe. Use deinit
to do this. Add the following to WhateverViewController
:
deinit { NSNotificationCenter.defaultCenter().removeObserver(self) } |
Finally, implement reinstateBackgroundTask()
.
func reinstateBackgroundTask() { if updateTimer != nil && (backgroundTask == UIBackgroundTaskInvalid) { registerBackgroundTask() } } |
You only need to reinstate if there is a timer going but the background task is invalid.
Breaking your code into smaller utility functions that do one thing is paying off. You simply need to call registerBackgroundTask()
when a background task not running for the current timer.
And there you have it. You can download the partially finished sample project up to this point.
On to the final topic for this tutorial: Background Fetching.
Background Fetch
Background fetch is a mode introduced in iOS 7 that lets your app appear always up-to-date with the latest information while minimizing the impact on battery. Suppose, for example, you were implementing a news feed in your app. Prior to background fetch, you might do this by getting new data in viewWillAppear(_:)
. The problem with this solution is that your user is looking at the old data for at least several seconds until the new data comes in. Wouldn’t it be better if right when they opened your app the new data was magically there? This is what background fetch gives you.
When enabled, the system uses usage patterns to determine when to best fire off a background fetch. For example, if your user opens the app at 9 AM each morning, it is likely that a background fetch will be scheduled sometime before that time. The system decides the best time to issue a background fetch and for this reason you should not use it to do critical updates.
In order to implement background fetch there are three things you must do:
- Check the box Background fetch in the Background Modes of your app’s Capabilities.
- Use
setMinimumBackgroundFetchInterval(_:)
to set a time interval appropriate for your app. - Implement
application(_:performFetchWithCompletionHandler:)
in your app delegate to handle the background fetch.
As the name implies, background fetch normally involves fetching information from an external source like a network service. For the purposes of this tutorial, you won’t use the network but just fetch the current time. This simplification will let you understand everything required to perform a background fetch and test it without worrying about an external service.
In contrast to finite-length tasks, you only have seconds to operate when doing a background fetch – the consensus figure is a maximum of 30 seconds, but shorter is better. If you need to download large resources as part of the fetch, this is where you need to use NSURLSession
‘s background transfer service.
Time to get started. First, open FetchViewController.swift and add the following property and method to FetchViewController
.
var time: NSDate? func fetch(completion: () -> Void) { time = NSDate() completion() } |
This code is a simplified replacement of what you might actually do to fetch some data from an external source (such as a JSON or XML RESTful service). Since it might take several seconds to fetch and parse the data, you pass a completion handler that gets called when the process is done. You will see why this is important a little later.
Next, finish off the view controller. Add the following methods to FetchViewController
.
func updateUI() { if let time = time { let formatter = NSDateFormatter() formatter.dateStyle = .ShortStyle formatter.timeStyle = .LongStyle updateLabel?.text = formatter.stringFromDate(time) } else { updateLabel?.text = "Not yet updated" } } override func viewDidLoad() { super.viewDidLoad() updateUI() } @IBAction func didTapUpdate(sender: UIButton) { fetch { self.updateUI() } } |
updateUI()
formats the time and shows it. It is an optional type so if it is not set, it shows the message not updated yet. When the view is first loaded (in viewDidLoad()
) you don’t fetch, but go straight to updateUI()
which means the message “Not yet updated” will appear first. Finally, when the Update button is tapped it performs a fetch and as a completion updates the UI.
At this point, the view controller is working.
However, background fetching is not enabled.
The first step to enabling background fetching is to check Background fetch in the Capabilities tab of your app. By now this should be old hat. Go ahead and do this.
Next, open AppDelegate.swift and request background fetches in application(_:didFinishLaunchingWithOptions:)
by setting the minimum background fetch interval.
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool { UIApplication.sharedApplication().setMinimumBackgroundFetchInterval( UIApplicationBackgroundFetchIntervalMinimum) return true } |
The default interval is UIApplicationBackgroundFetchIntervalNever
which you might want to switch back to, if, for example your user logs out and no longer needs updates. You can also set a specific interval in seconds. The system will wait at least that many seconds before issuing a background fetch.
Be careful not to set the value too small because it will chew through battery unnecessarily as well as hammer your server. In the end the exact timing of the fetch is up to the system but will wait at least this interval before performing it. Generally, UIApplicationBackgroundFetchIntervalMinimum
is a good default value to use.
Finally, to enable background fetch you must implement application(_:performFetchWithCompletionHandler:)
. Add the following method to AppDelegate.swift:
// Support for background fetch func application(application: UIApplication, performFetchWithCompletionHandler completionHandler: (UIBackgroundFetchResult) -> Void) { if let tabBarController = window?.rootViewController as? UITabBarController, viewControllers = tabBarController.viewControllers as? [UIViewController] { for viewController in viewControllers { if let fetchViewController = viewController as? FetchViewController { fetchViewController.fetch { fetchViewController.updateUI() completionHandler(.NewData) } } } } } |
First you need to get the FetchViewContoller
. Next, optionally cast since the rootViewController
is not necessarily a UITabBarController
in every app, although it is in this app so it will never fail.
Next, you loop though all of the view controllers in the tab bar controller and find the one that successfully casts to FetchViewController
. In this app you know it is the last view controller so you could hard-code it, but looping makes it a little more robust in case you decide to add or remove a tab later.
Finally you call fetch(_:)
. When it completes, you update the UI and then call the completionHandler
that was passed in as a parameter of the function. It is important that you call this completion handler at the end of the operation. You specify what happened during the fetch as the first parameter. Possible values are .NewData
, .NoData
, or .Failed
.
For simplicity, the tutorial always specifies .NewData
since getting the time will never fail and is always different than the last call. iOS can then use this value to better time the background fetches. The system knows at this point to take a snapshot of the app so it can present it in the card view of the app switcher. And that is all there is to it.
Note: Rather than passing the completion closure along, it can be tempting to save it away in a property variable and call that when your fetch completes. Don’t do this. If you get multiple calls to application(_:performFetchWithCompletionHandler:)
, the previous handler will get overwritten and never called. It is better to pass the handler through and call it as it will make this kind of programming error impossible.
Testing Background Fetch
One way to test background fetch is to sit around and wait for the system to decide to do it. That would require a lot of sitting. Fortunately, Xcode gives a way to simulate a background fetch. There are two scenarios that you need to test. One is when your app is in the background, and the other when your app is coming back from being suspended. The first way is the easiest and is just a menu selection.
- Start the app on an actual device (not a simulator);
- From the Debug menu in Xcode, select Simulate Background Fetch;
- Reopen the app, noticing that it got sent to the background;
- Switch to the Fetch tab, (Notice that the time is updated to when you simulated the background fetch and not “Not yet updated”.)
The other way you should test background fetch is to do it as a resume from suspension. There is a launch option that lets you launch your app directly into suspension. Since you might want to test this semi-often it is best to make a new Scheme with that option always set. Xcode makes this easy.
First select the Manage Schemes option.
Next, select the only scheme in the list, and then click on the gear icon and select Duplicate Scheme.
Lastly, rename your scheme to something reasonable, like, “Background Fetch” and check the checkbox to Launch due to background fetch event.
Note that in Xcode 6.1, this doesn’t work reliably with the simulator. In my own testing, I needed to use an actual device to launch into the suspended mode correctly.
Run your app with this scheme. You will notice that the app never actually opens but is launched into a suspended state. Now manually launch it and go to the Fetch tab. You should see that there is an updated time when you launched the app instead of “Not yet updated”.
Using background fetch effectively lets your users to seamlessly get the latest content.
Where to Go From Here?
You can download the fully finished sample project here.
If you want to read Apple’s documentation on what we covered here, the best place to start is in Background Execution. 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.
Finally, be sure to check out NSURLSession if you plan on doing large network transfers 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.
Background Modes Tutorial: Getting Started is a post from: Ray Wenderlich
The post Background Modes Tutorial: Getting Started appeared first on Ray Wenderlich.