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 a URLSessionDownloadTask. 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:

Screenshot%202021-05-05%20at%2009.08.27

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.

Screenshot%202021-05-05%20at%2009.10.47

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: Screenshot%202021-05-05%20at%2009.40.29

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.

Next Post Previous Post