0

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".

Mega
  • 36
  • 3
  • 1
    Since you are using a background task, you don't need to use a background URL session. You can use a regular URLSession. What you do need to do is pass a completion handler to `callAPI` so that you can invoke the `setTaskCompleted` only once your network operation is complete – Paulw11 Mar 14 '23 at 23:50

0 Answers0