iosswifturlsessionuibackgroundtaskbackgroundtaskidentifier

Swift Background Execution


I have a step function on AWS triggered by an HTTP Post request. The function can take a few seconds to complete. I'd like for execution to continue if the user puts the app into the background, and to correctly navigate to the next screen once the user puts the app back into the foreground (if execution has finished).

My API Client endpoint looks like this:

 func connect<OutputType: Decodable>(to request: URLRequestConvertible, decoder: JSONDecoder) -> AnyPublisher<Result<OutputType, Error>, Never> {
    var request = request.asURLRequest()
    
    if let token: String = KeychainWrapper.standard.string(forKey: "apiToken") {
        request.addValue(token, forHTTPHeaderField: "Authorization")
    }
    
    let configuration = URLSessionConfiguration.default
    configuration.waitsForConnectivity = true
    let session = URLSession(configuration: configuration)
    
    return session.dataTaskPublisher(for: request)
        .tryMap({ (data, response) -> Data in
            guard let response = response as? HTTPURLResponse else { throw NetworkError.invalidResponse }
            guard 200..<300 ~= response.statusCode else {
                throw NetworkError.invalidStatusCode(statusCode: response.statusCode)
                
            }
            return data
        })
        .decode(type: OutputType.self, decoder: decoder)
        .map(Result.success)
        .catch { error -> Just<Result<OutputType, Error>> in Just(.failure(error)) }
        .receive(on: DispatchQueue.main)
        .eraseToAnyPublisher()
}

I'd like to know the best practice for implementing this call. I'm currently using beginBackgroundTask below.

func makeRequest() {
    DispatchQueue.global(qos: .userInitiated).async {
        self.backgroundTaskID = UIApplication.shared.beginBackgroundTask (withName: "Request Name") {
            UIApplication.shared.endBackgroundTask(self.backgroundTaskID!)
            self.backgroundTaskID = .invalid
        }
        <implementation>
    }
}

However, implementation only works if I have nested DispatchQueue.main.async blocks where I perform more logic after making the HTTP request (like determining which screen to navigate to next after we receive the response.

Is this the best way to do it? Is it ok to have a few different nested DispatchQueue.main.async blocks inside the DispatchQueue.global block? Should I post the .receive(on: ) to DispatchQueue.global?


Solution

  • You don’t have to dispatch this background task to a background queue at all. (Don’t conflate the “background task”, which refers to the app state, with the “background queue”, which governs which threads are used.)

    Besides, as the documentation says, the expiration handler closure runs on the main thread:

    The system calls the handler synchronously on the main thread, blocking the app’s suspension momentarily.

    So you really want to keep all interaction with backgroundTaskID on the main thread, anyway, or else you would have to implement some other synchronization mechanism.


    And as a matter of good practice, make sure to end your background task when your asynchronous request is done (rather than relying on the expiration/timeout closure).