Using Swift 5.7 and Scenes
I want to make an HTTP call to an endpoint when the app goes to the background. My current implementation is not working and I am unsure on which part is incorrect. A lot of the resources online (SO, Apple docs) are outdated, so I am having a tough time finding examples.
I am using a custom URLSessionDataDelegate
to implement the callbacks for when the API call is complete. My understanding is that I have to use a dataTask
in a background task, since async
/await
calls won't work in the background. I would also like to make the background task submit a new background task for when it completes, but I haven't figured out how to do that.
This is what I currently have:
AppDelegate.swift
let bgTaskIdentifier = "task.identifier"
class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
BGTaskScheduler.shared.register(forTaskWithIdentifier: bgTaskIdentifier, using: nil) { task in
self.handleAppRefresh(task as! BGAppRefreshTask)
}
print("Registered background task (\(bgTaskIdentifier))")
return true
}
func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
}
func handleAppRefresh(_ task: BGAppRefreshTask) {
task.expirationHandler = {
task.setTaskCompleted(success: false)
}
Service().callAPI()
task.setTaskCompleted(success: true)
}
}
SceneDelegate.swift
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let _ = (scene as? UIWindowScene) else {
return
}
}
func sceneDidEnterBackground(_ scene: UIScene) {
submitBackgroundTask()
}
func submitBackgroundTask() {
do {
let taskRequest = BGAppRefreshTaskRequest(identifier: bgTaskIdentifier)
taskRequest.earliestBeginDate = Date(timeIntervalSinceNow: 10)
try BGTaskScheduler.shared.submit(taskRequest)
logger.info("Submitted background task request")
} catch {
logger.info("Failed to submit task request")
}
}
}
Service.swift
class Service: NSObject, URLSessionDataDelegate {
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
let decoder = JSONDecoder()
// Decode here and do stuff with response data
}
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse, completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) {
guard let response = response as? HTTPURLResponse, (200...299).contains(response.statusCode) else {
logger.info("Request failed")
completionHandler(.cancel)
return
}
completionHandler(.allow)
}
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
if error != nil {
logger.info("Failed: \(error!)")
}
}
func callAPI() {
let url = URL(string: "https://example.com")!
var request = URLRequest(url: url)
let config = URLSessionConfiguration.background(withIdentifier: bgTaskIdentifier)
let session = URLSession(configuration: config, delegate: self, delegateQueue: nil)
session.dataTask(with: request).resume()
}
}
My hunch is that since Service().callAPI()
is not async, task.setTaskCompleted(success: true)
gets called before the data task is completed, thus not running the entire handler. I tried to pass the task to the service so that task.setTaskCompleted(success: true)
can be called in the delegate functions but that didn't work.
I also am often getting errors that say: NSURLErrorDomain Code=-997 "Lost connection to background transfer service"
.