Developers and users are often confused as to what the iOS multitasking system allows. Apple has restrictions in place for the use of background operations in an effort to improve user experience and extend battery life. Your app is only allowed to keep running in the background in very specific cases. For example, these include fetching the latest content from a server, getting location updates, downloading videos or playing audio.
This project demonstrate an app that uses the following background modes:
Apple introduced a new framework called BackgroundTasks
for scheduling background work. This new framework does better support for tasks that are needed to be done in the background. There are two types of background tasks: BGAppRefreshTask
and BGProcessingTask
.
-
BGAppRefreshTask
is for short-duration tasks that expect quick results, such as fetching data from the server. BGAppRefreshTask can have 30 seconds to complete its job. BGAppRefreshTask can be used for following tasks:- Fetching a social feeds from the server
- Fetching news feed
-
BGProcessingTask
is for tasks that might be time-consuming, such as downloading a large file or synchronizing data. Your app can use one or both of these. BGProcessingTask can have more than a minute to complete its job. BGProcessingTask can be used for following tasks:- Core ML training
- Data synchronization
- Database cleanup
In the demo using BGAppRefreshTask, put the app into the background and simulate a background app refresh. Later, open the app and you will see the weather information already fetched from the server as following screenshots:
In the demo using BGProcessingTask, put the app into the background and simulate a background processing. Later, open the app and you will see the total number of photos and videos are fetched from the photo library as following screenshots:
iOS allows app to refresh it content even when it is sent to background. iOS can intelligently study the user’s behaviour and schedule background tasks to the moment right before routine usage. It is useful for app to retrieve the latest information from its server and display to user right when app is resumed to foreground. Examples are social media app (Facebook, Instagram & WhatsApp) and news app. Following is illustration of foreground and background task:
To configure your app to allow background tasks, enable the background capabilities that you need, and then create a list of unique identifiers for each task.
To add the capabilities:
- Click Signing & Capabilities.
- Expand the Background Modes section. If the target doesn’t have a Background Modes section, click + Capability, and then select Background Modes.
- If you’re using BGAppRefreshTask, select ”Background fetch.“
- If you’re using BGProcessingTask, select ”Background processing.“
To create this list, add the identifiers to the Info.plist file.
- Open the project navigator and select your target.
- Click Info and expand Custom iOS Target Properties.
- Add a new item to the list and choose ”Permitted background task scheduler identifiers“.
- Add the string for each authorized task identifier as a separate item in the array.
For each task, provide the BGTaskScheduler object with a launch handler (a small block of code that runs the task) and a unique identifier. Register all of the tasks before the end of the app launch sequence. To register background tasks, inside the application(_:didFinishLaunchingWithOptions)
method, we should add the following command.
// For Background fetch
BGTaskScheduler.shared.register(forTaskWithIdentifier: "com.pratik.backgrounds.BGFetch", using: nil) { task in
self.handleAppRefresh(task: task as! BGAppRefreshTask)
}
// For Background processing
BGTaskScheduler.shared.register(forTaskWithIdentifier: "com.pratik.backgrounds.BGProcessing", using: nil) { task in
self.handleBackgroundProcessing(task: task as! BGProcessingTask)
}
To submit a task request for the system to launch your app in the background at a later time, use submit(_:)
.
// For Background fetch
func scheduleAppRefresh() {
let request = BGAppRefreshTaskRequest(identifier: "com.pratik.backgrounds.BGFetch")
// Fetch no earlier than 15 minutes from now
request.earliestBeginDate = Date(timeIntervalSinceNow: 15 * 60)
do {
try BGTaskScheduler.shared.submit(request)
} catch {
print("Could not schedule app refresh: \(error)")
}
}
// For Background processing
func scheduleBackgroundProcessing() {
let request = BGProcessingTaskRequest(identifier: "com.pratik.backgrounds.BGProcessing")
// Fetch no earlier than 15 minutes from now
request.earliestBeginDate = Date(timeIntervalSinceNow: 15 * 60)
// System will launch your app only when the device has a network connection
request.requiresNetworkConnectivity = false
// System will launch your app only while the device is connected to external power
request.requiresExternalPower = false
do {
try BGTaskScheduler.shared.submit(request)
} catch {
print("Could not schedule background processing: \(error)")
}
}
When the system opens your app in the background, it calls the launch handler to run the task.
// For Background fetch
func handleAppRefresh(task: BGAppRefreshTask) {
// Schedule a new refresh task
self.scheduleAppRefresh()
// Provide the background task with an expiration handler that cancels the operation
task.expirationHandler = {
task.setTaskCompleted(success: false)
}
// Do some data fetching and inform the system
let isFetchSuccess = true // or false if task failed
task.setTaskCompleted(success: isFetchSuccess)
}
// For Background processing
func handleBackgroundProcessing(task: BGProcessingTask) {
// Schedule a new refresh task.
self.scheduleBackgroundProcessing()
...
}
The delay between the time you schedule a background task and when the system launches your app to run the task can be many hours. While developing your app, you can use a private functions to start a task.
To launch a task:
- Pause app at any point after submitting the BGTask to BGTaskScheduler
- Run the following command at the Terminal in Xcode
e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"YOUR_TASK_IDENTIFIER"]
- Resume back your app. You can see the completion handler of the registered BGTask is then triggered.
If your app’s server-based content changes infrequently or at irregular intervals, you can use background notifications to notify your app when new content becomes available. A background notification is a remote notification that doesn’t display an alert, play a sound, or badge your app’s icon. It wakes your app in the background and gives it time to initiate downloads from your server and update its content.
In the demo using background notifications, while app is killed (even not running in the background) - send a background notifiation. Later, open the app and you will see the updated drinks menu already fetched from the server as following screenshots:
iOS allows app to refresh it content even when it is sent to background. In case of Background Tasks
, system study the user’s behaviour and schedule background tasks. In case of Remote Notifications
, app is awakened in the background using the push notification.
In most cases, silent push notifications are used in background content update. The background operation triggered by a background push notification will have roughly 30 seconds of execution time.
To receive background notifications, you must add the remote notifications background mode to your app. In the Signing & Capability tab, add the Background Modes
capability, then select the Remote notification checkbox.
Also, we need to add Push Notifications
capabilities in Xcode in order for your app to be able to receive a silent push notification.
To send a background notification, create a remote notification with an aps dictionary that includes the content-available
key. You may include custom keys in the payload, but the aps dictionary must not contain any keys that would trigger user interactions. The notification’s POST request should contain the the apns-priority
field with a value of 5.
Sample payload for a background notification:
{
"aps" : {
"content-available" : 1
"apns-priority" : 5
},
"userId" : 73, // custom key 1
"userName" : "Pratik" // custom key 2
}
To deliver a background notification, the system wakes your app in the background. On iOS it then calls your app delegate’s application(_:didReceiveRemoteNotification:fetchCompletionHandler:)
method. The code snippet below demonstrates how you can extract data from the notification payload.
func application(_ application: UIApplication,
didReceiveRemoteNotification userInfo: [AnyHashable : Any],
fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void)
{
if let value = userInfo["userName"] as? String {
print(value) // output: "Pratik"
}
// Fetch the data from server
guard let url = URL(string: "YOUR_SERVER_ENDPOINT") else {
completionHandler(.failed)
return
}
let task = URLSession.shared.dataTask(with: url) { data, response, error in
if let error = error {
print("Error while fetching data from server: \(error.localizedDescription)")
completionHandler(.failed)
return
}
guard response != nil else {
print("No response found from the server")
completionHandler(.noData)
return
}
guard let data = data else {
print("No data found from the server")
completionHandler(.noData)
return
}
self.storeDataInDatabase(data)
// Inform the system after the background operation is completed.
completionHandler(.newData)
}
}
Notice that you must call the completion handler in order to inform the system after the background operation is completed.
Technically, this is not a background mode, as you don’t have to declare that your app uses this mode in Capabilities. Instead, it’s an API that lets you run arbitrary code for a finite amount of time when your app is in the background. Extending your app’s background execution time ensures that you have sufficient time to perform critical tasks.
When your app moves to the background, the system calls your app delegate’s applicationDidEnterBackground(_:)
method. That method has five seconds to perform any tasks and return. Shortly after that method returns, the system puts your app into the suspended state. For most apps, five seconds is enough to perform any crucial tasks, but if you need more time, you can ask UIKit to extend your app’s runtime.
In the demo using background extension, put the app into the background and wait intentionally for 20 seconds as task is peformed when 10 seconds are remaining. This way, we can be sure that the task is performed even after 5 seconds when app was send to background. Later, open the app and you will see the USD exchange rates already fetched from the server as following screenshots:
Use Background Extension
when leaving a task unfinished might cause a bad user experience in your app. For example, call this method before writing data to a file to prevent the system from suspending your app while the operation is in progress.
Normally the iOS will give upto 3 minutes to complete the task. This is just a general observation. The time can be greater than or less than 3 minutes as it is not given in the official documentation.
Background Extension
can be used for following other tasks:
- Complete disk writes
- Finish user-initiated requests
- Network calls
- Apply filters to images
- Render a complicated 3D mesh
You first need to add the following property to the place where you are performing the task. This property identifies the task request to run in the background.
var backgroundTask: UIBackgroundTaskIdentifier = .invalid
Next, lets register a task:
self.backgroundTask = UIApplication.shared.beginBackgroundTask(withName: "Get Exchange Rates") {
print("--------------------------------")
print("System is about to kill our task")
print("--------------------------------")
// End the task if time expires.
self.endBackgroundTask()
}
assert(backgroundTask != .invalid)
UIApplication.beginBackgroundTask(expirationHandler:)
tells iOS that you need more time to complete whatever it is that you’re doing in case the app moves to the background. After this call, if your app moves to the background, it will still get CPU time until you call endBackgroundTask()
. If you provide a block object in the expirationHandler
parameter, the system calls your handler before time expires to give you a chance to end the task. If you provide it the system will call it before the time expires to give you a chance to end a task before it had time to complete.
You can call this method at any point in your app’s execution. You may also call this method multiple times to mark the beginning of several background tasks that run in parallel. However, each task must be ended separately. You identify a given task using the value returned by this method.
Each call to beginBackgroundTask API
should have a matching call to UIApplication.endBackgroundTask(identifier:)
. Because apps cannot run indefinitely in the background, you can check how much time your app has by checking the backgroundTimeRemaining
property of UIApplication.
If you don’t call endBackgroundTask()
after a period of time in the background, iOS will call the closure defined when you called beginBackgroundTask(expirationHandler:)
to give you a chance to stop executing code. So it’s a very good idea to then call endBackgroundTask()
to tell the iOS that you’re done. If you don’t do this and you continue to execute code after this block runs, iOS will terminate your app. endBackgroundTask()
indicates to iOS that you don’t need any extra CPU time.
func endBackgroundTask()
{
if self.backgroundTask != .invalid {
UIApplication.shared.endBackgroundTask(self.backgroundTask)
}
self.backgroundTask = .invalid
}
You should call endBackgroundTask()
at two places:
- In the expiration handler
- Once the task is completed
Your overall code can be implemented as below:
func sendDataToServer(data: NSData)
{
// Request the task assertion and save the ID.
self.backgroundTaskID = UIApplication.shared.
beginBackgroundTask (withName: "Finish Network Tasks") {
// End the task if time expires.
UIApplication.shared.endBackgroundTask(self.backgroundTaskID!)
self.backgroundTaskID = UIBackgroundTaskInvalid
}
// Send the data synchronously.
self.sendAppDataToServer(data: data)
// End the task assertion.
UIApplication.shared.endBackgroundTask(self.backgroundTaskID!)
self.backgroundTaskID = UIBackgroundTaskInvalid
}
For testing purpose, You can put a timer which fires every second and print remaining background execution time by following code.
let backgroundTimeLeft = UIApplication.shared.backgroundTimeRemaining
if UIApplication.shared.applicationState == .background {
print("Background time remaining = \(backgroundTimeLeft) seconds")
}
IMPORTANT: The value returned by
backgroundTimeRemaining
is an estimate and can change at any time. You must design your app to function correctly regardless of the value returned. It’s reasonable to use this property for debugging but we strongly recommend that you avoid using as part of your app’s logic.
We can to use URLSessionDownloadTask
to download files in the background so that they can completed even if the app is terminated. The actual download process will run outside of the app process by the system so it can continue when the app is terminated. This works exactly the same for uploads with URLSessionUploadTask
.
The demo shows how to implement background downloading with progress monitoring for multiple downloads running in parallel. Once all the downloads are completed, it will notify a user with a local notification, so the user can know that all the requested video downloading is finished.
Once the app is put into background state, the app is no longer connected to the internet and downlading does not continue. For example, the download does not continue while we put the device aside and it goes to sleep. What we need is the download should continue when user put app into background, keep device aside and display goes to sleep mode or while using another app.
In order to create a session for initiating download or upload tasks either on the background or the foreground, we need to use URLSessionConfiguration
class. Create a background URLSessionConfiguration
object with the class method background(withIdentifier:)
of URLSession
, providing a session identifier that is unique within your app.
The next step that must be performed, is to instantiate the URLSession
instance using this URLSessionConfiguration
instance. Provide a delegate, to receive events from the background transfer.
private lazy var session: URLSession = {
let configuration = URLSessionConfiguration.background(withIdentifier: "com.pratik.backgroundDownloads")
configuration.sessionSendsLaunchEvents = true
return URLSession(configuration: configuration, delegate: self, delegateQueue: nil)
}()
You create download tasks from the session with either the downloadTask(with:)
method that takes a URL
, or the downloadTask(with:)
method that takes a URLRequest
instance.
let downloadTask = session.downloadTask(with: url)
downloadTask.resume()
Every time that the background thread of the system that is responsible for a file download of our app has messages for it, it calls the application:handleEventsForBackgroundURLSession:completionHandler:
application delegate method. By doing so, the session identifier that woke the app up, along with a completion handler are passed to the app. The completion handler of the parameter must be stored locally, and called when all downloads are finished so the system knows that no more background activity is required (all transfers are complete) and any reserved resources to be freed up. Of course, upon each download finish, the URLSession:downloadTask:didFinishDownloadingToURL:
delegate method is called to do all the finishing actions, such as copying the downloaded file from the temporary location to the Documents directory.
var backgroundTransferCompletionHandler: () -> ()? = nil
func application(_ application: UIApplication,
handleEventsForBackgroundURLSession identifier: String,
completionHandler: @escaping () -> Void) {
self.backgroundTransferCompletionHandler = completionHandler
}
Other than the above method, when the system has no more messages to send to our app after a background transfer, the urlSessionDidFinishEvents(forBackgroundURLSession:)
method of URLSessionDelegate
method is called. In that method we will make the call to the completion handler, and we will show the local notification.
In the implementation, we first make sure that all downloads are over. Once that’s true, we store locally the completion handler and make nil the backgroundTransferCompletionHandler property. The call to the local copy of completion handler must always take place in the main thread.
func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession)
{
session.getTasksWithCompletionHandler { (dataTasks, uploadTasks, downloadTasks) in
// Check if all download tasks have been finished.
if downloadTasks.count == 0 {
// Copy locally the completion handler.
if let completionHandler = self.backgroundTransferCompletionHandler {
OperationQueue.main.addOperation({
// Call the completion handler to tell the system that there are no other background transfers.
completionHandler()
// Show a local notification when all downloads are over.
})
// Make nil the backgroundTransferCompletionHandler.
self.backgroundTransferCompletionHandler = nil;
}
}
}
}
If you play audio from streaming data, you start a network connection, and the connection callbacks provide continuous audio data. In iOS, when you activate the Audio background mode
, iOS will continue these callbacks even if your app is not the current active app. In short, the Audio background mode is virtually automatic. You just have to activate it and provide the infrastructure to handle it appropriately.
While playing a music, tap the Home button and the music will stop. Use Audio background mode
when you want to keep audio to play even while user puts app into background.
You need to enable the capability to indicate that the app wants to plau audio while in the background mode.
The demo for Background Audio Playback makes use of an AVQueuePlayer
to queue songs and play them one after the other.
func configureAudioSesion()
{
// Set the audio session’s category, mode and options.
do {
try AVAudioSession.sharedInstance().setCategory(
AVAudioSession.Category.playAndRecord,
mode: .default,
options: [.defaultToSpeaker, .allowBluetooth])
} catch {
print("Failed to set audio session category: \(error.localizedDescription)")
}
}
The controller is observing the player’s currentItem
value to provide updates for player's current item. Set the song metadata in that observer.
override func observeValue(forKeyPath keyPath: String?, of object: Any?,
change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?)
{
if keyPath == "currentItem",
let player = object as? AVPlayer,
let currentItem = player.currentItem?.asset as? AVURLAsset {
self.showSongMetada(currentItem)
}
}
func showSongMetada(_ playerItem: AVURLAsset)
{
let metadata = playerItem.commonMetadata
// 1. Album artwork
let artworkItems = AVMetadataItem.metadataItems(from: metadata, filteredByIdentifier: .commonIdentifierArtwork)
if let artworkItem = artworkItems.first, let imageData = artworkItem.dataValue {
let image = UIImage(data: imageData)
self.albumImageView.image = image
} else {
self.albumImageView.image = UIImage(named: "albumPlaceholder")
}
// 2. Song name
let albumItems = AVMetadataItem.metadataItems(from: metadata, filteredByIdentifier: .commonIdentifierTitle)
if let albumItem = albumItems.first, let albumName = albumItem.value as? String {
self.songNameLabel.text = albumName
} else {
self.songNameLabel.text = "--"
}
// 3. Artist Name
let artistItems = AVMetadataItem.metadataItems(from: metadata, filteredByIdentifier: .commonIdentifierArtist)
if let artistItem = artistItems.first, let artistName = artistItem.value as? String {
self.artistNameLabel.text = artistName
} else {
self.artistNameLabel.text = "--"
}
// 4. Progress
self.progressView.progress = 0
// 5. Playback duration
self.playbackTimeLabel.text = "00:00"
// 6. Total duration
let duration: CMTime = playerItem.duration
let seconds: Float64 = CMTimeGetSeconds(duration)
self.totalTimeLabel.text = Helper.stringFromTimeInterval(seconds)
}
We can request a block during playback to get changing time during normal playback, according to progress of the current time of the player.
func addPlaybackTimeObserver()
{
let interval = CMTimeMake(value: 1, timescale: 100)
self.timeObserverToken = self.player.addPeriodicTimeObserver(forInterval: interval, queue: .main) { [weak self] progress in
guard let self = self else { return }
let timeString = Helper.stringFromTimeInterval(progress.seconds)
if UIApplication.shared.applicationState == .active {
// 1. Progress label
self.playbackTimeLabel.text = timeString
// 2. Progress bar
if let currentItem = self.player.currentItem?.asset as? AVURLAsset {
let progress = progress.seconds/currentItem.duration.seconds
self.progressView.progress = Float(progress)
}
} else {
print("Background: \(timeString)")
}
}
}