Networking with Background Tasks in iOS 13
Reading time: 4 min
This recipe shows how to perform a GET request in the background using BGTaskScheduler
and BGAppRefreshTaskRequest
. For whatever reason, this has way too many gotchas:
- You can't use Alamofire, you have to use
URLSession
. - You can use
URLSessionDataTask
, it has to be aURLSessionDownloadTask
. Why? Beats me. - You can't use callbacks, you must use delegates to track request progress and completion.
All this info is scattered around the web and in the docs, making it difficult to come up with a complete solution that works, so keep reading!
Step #1: Prep background task capabilities
First add the Background fetch capability to your project:
Next, go to Info and add Permitted background task scheduler identifiers
key. Then, add a single item to it, that'll be the ID of your background task.
Then, copy this ID as a constant somewhere in your code (wherever it is that you see fit):
let backgroundTaskID = "net.globulus.TestBackgroundFetch"
Step #2: Register and submit background task
Open AppDelegate.swift and add these two methods to it:
private func registerBackgroundFetch() {
// This code registers your background fetch task.
BGTaskScheduler.shared.register(forTaskWithIdentifier: backgroundTaskID, using: nil) { task in
// You must set an expiration handler that calls `task.setTaskCompleted(success: false)`
task.expirationHandler = {
print("Task expired")
task.setTaskCompleted(success: false)
}
print("Refreshing app in background. Time remaining: \(UIApplication.shared.backgroundTimeRemaining) s")
// Your networking code. You'll implement this one in the next step.
let networking = Networking()
networking.get { result in
switch result {
case .success(let response):
print("Everything's fine! Got response: \(response)")
task.setTaskCompleted(success: true) // important
case .failure(let error):
print("Error fetching new status: \(error)")
task.setTaskCompleted(success: false) // important
}
}
}
}
func submitBackgroundTasks() {
// This code schedules your background task when the app goes to background.
do {
let request = BGAppRefreshTaskRequest(identifier: backgroundTaskID)
request.earliestBeginDate = Date().addingTimeInterval(TimeInterval(60)) // 1 min later, set to whatever you want
try BGTaskScheduler.shared.submit(request)
print("Submitted background task")
} catch {
print("Failed to submit background task: \(error)")
}
}
Then, update application(_ application:didFinishLaunchingWithOptions:)
to call registerBackgroundFetch
:
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
registerBackgroundFetch()
return true
}
On the other hand, submitBackgroundTasks
should be called when your app goes to background. If you're using UIKit, it should go into applicationDidEnterBackground(:)
. If you have a SceneDelegate.swift, add it to sceneDidEnterBackground(:)
:
func sceneDidEnterBackground(_ scene: UIScene) {
(UIApplication.shared.delegate as? AppDelegate)?.submitBackgroundTasks()
}
Step #3: Implement networking
Here's sample code on what your networking can look like. Again, you must use URLSession
with download task and delegates:
typealias Callback<T> = (Result<T, Error>) -> Void
// Mock model
struct MyModel: Codable {
let title: String
let subtitle: String
}
class Networking: NSObject {
private var backgroundDownloadTask: URLSessionDownloadTask?
private var backgroundCallback: Callback<MyModel>?
func get(callback: @escaping Callback<MyModel>) {
guard let url = URL(string: "my-url-here")
else {
callback(.failure(URLError(.badURL)))
return
}
// Notice the URLSessionConfiguration.background, usage of delegates, and downloadTask
let session = URLSession(configuration: URLSessionConfiguration.background(withIdentifier: backgroundTaskID),
delegate: self,
delegateQueue: nil)
let task = session.downloadTask(with: url)
backgroundDownloadTask = task
backgroundCallback = callback
task.resume()
}
}
extension Networking: URLSessionTaskDelegate {
// Implement this method to handle download task errors.
func urlSession(_ session: URLSession,
task: URLSessionTask,
didCompleteWithError error: Error?) {
if let error = error {
backgroundCallback?(.failure(error))
backgroundCallback = nil
}
}
}
extension Networking: URLSessionDownloadDelegate {
// This is ridiculous but it's what we have to do - read JSON from the temp file
// download task created for us.
func urlSession(_ session: URLSession,
downloadTask: URLSessionDownloadTask,
didFinishDownloadingTo location: URL) {
guard downloadTask.originalRequest?.url == backgroundDownloadTask?.originalRequest?.url
else {
return
}
print("Finished downloading background task: \(location)")
do {
let jsonData = try Data(contentsOf: location, options: [])
let model = try JSONDecoder().decode(MyModel.self, from: jsonData)
backgroundCallback?(.success(model))
} catch {
backgroundCallback?(.failure(error))
}
backgroundDownloadTask = nil
backgroundCallback = nil
}
}
Step #4: See it in action
First, put a breakpoint at this line:
Then, run your app on a physical device. Background modes don't seem to work on simulators yet. Once the app runs, press the home button to put it into background, and your breakpoint will trigger.
Type this in the debugger console:
e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"net.globulus.TestBackgroundFetch"]
Press enter, and you'll get a response:
Simulating launch for task with identifier net.globulus.TestBackgroundFetch
Lastly, resume the execution of your app and the background task will run, alongside your network call.
That's it! Hopefully these limitations are resolved in a future version of iOS.