Search code examples
iosswiftcore-locationbackground-processurlsession

Forwarding user's location to server when application is in the background (Swift/iOS)


I need to keep my server updated with user's location even when the app is in the background or terminated.

The location updating is working just fine and seems to wake the application as wanted.

My problem is regarding the forwarding of the user's location via a PUT request to the server. I was able to go through the code with breakpoints and it goes well except that when I check with Charles if requests are going though, nothing appears.

Here is what I have so far:

API Client

final class BackgroundNetwork: NSObject, BackgroundNetworkInterface, URLSessionDelegate {
    private let keychainStorage: Storage
    private var backgroundURLSession: URLSession?

    init(keychainStorage: Storage) {
        self.keychainStorage = keychainStorage

        super.init()

        defer {
            let sessionConfiguration = URLSessionConfiguration.background(withIdentifier: "backgroundURLSession")
            sessionConfiguration.sessionSendsLaunchEvents = true
            sessionConfiguration.allowsCellularAccess = true

            backgroundURLSession = URLSession(configuration: sessionConfiguration,
                                              delegate: self,
                                              delegateQueue: nil)
        }
    }

    func put<T: Encodable>(url: URL, headers: Headers, body: T) {
        var urlRequest = URLRequest(url: url)

        urlRequest.httpMethod = "PUT"

        let authenticationToken: String? = try? keychainStorage.get(forKey: StorageKeys.authenticationToken)
        if let authenticationToken = authenticationToken {
            urlRequest.setValue(String(format: "Bearer %@", authenticationToken), forHTTPHeaderField: "Authorization")
        }

        headers.forEach { (key, value) in
            if let value = value as? String {
                urlRequest.setValue(value, forHTTPHeaderField: key)
            }
        }

        do {
            let jsonData = try JSONEncoder().encode(body)
            urlRequest.httpBody = jsonData
        } catch {
            #if DEBUG
            print("\(error.localizedDescription)")
            #endif
        }

        backgroundURLSession?.dataTask(with: urlRequest)
    }
}

AppDelegate

// ...
        func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
            if launchOptions?[UIApplication.LaunchOptionsKey.location] != nil {
                environment.locationInteractor.backgroundDelegate = self
                _ = environment.locationInteractor.start()
            }
    
            return true
        }

// ...

    extension AppDelegate: LocationInteractorBackgroundDelegate {
        func locationDidUpdate(location: CLLocation) {
            taskId = UIApplication.shared.beginBackgroundTask {
                UIApplication.shared.endBackgroundTask(self.taskId)
                self.taskId = .invalid
            }
    
            environment.tourInteractor.updateLocationFromBackground(latitude: Float(location.coordinate.latitude),
                                                                    longitude: Float(location.coordinate.longitude))
    
            UIApplication.shared.endBackgroundTask(taskId)
            taskId = .invalid
        }
    }

SceneDelegate (yes, the application is using SwiftUI and Combine and I target iOS 13 or later)

func sceneWillEnterForeground(_ scene: UIScene) {
    if let environment = (UIApplication.shared.delegate as? AppDelegate)?.environment {
        environment.locationInteractor.backgroundDelegate = nil
    }
}

func sceneDidEnterBackground(_ scene: UIScene) {
    if let appDelegate = (UIApplication.shared.delegate as? AppDelegate) {
        appDelegate.environment.locationInteractor.backgroundDelegate = appDelegate
        _ = appDelegate.environment.locationInteractor.start()
    }
}

So basically, whenever my app goes in background, I set my delegate, restart the location updates and whenever an update comes, my interactor is called and a request is triggered.

According to breakpoints, eveything just works fine up to backgroundURLSession?.dataTask(with: urlRequest). But for some reason the request never gets fired.

I obviously checked Background Modes capabilities Location updates and Background fetch.

Any idea why ?


Solution

  • That’s correct, the line

    backgroundURLSession?.dataTask(with: urlRequest)
    

    does nothing. The way to do networking with a session task is to say resume, and you never say that. Your task is created and just thrown away. (I’m surprised the compiler doesn’t warn about this.)