2

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?

mfaani
  • 33,269
  • 19
  • 164
  • 293
Eric Jubber
  • 131
  • 1
  • 9

1 Answers1

2

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

Rob
  • 415,655
  • 72
  • 787
  • 1,044
  • By "calls the handler synchronously on the main thread ": Is that the expirationhandler or the actual block associated with the task? i.e. is it `mainQueue.sync(item: expirationhandlerItem)` or `mainQueue.sync(item: theactulTaskItem)` – mfaani Feb 18 '22 at 19:34
  • 1
    The expiration handler closure is dispatched to the main queue. But it doesn't block the main thread for the duration, but rather the documentation is telling you that it calls the completion handler synchronously when you run out of time for your background task. So, just have your background task do whatever it needs, like normal, following the standard, good practice, asynchronous patterns, and you will be fine. Just don’t block the main thread (which you never would do, anyway). – Rob Feb 18 '22 at 19:54
  • Gotcha. I suppose it's `sync` because if it's `async`, then there's a chance that the expirationHandler may get called too late i.e. after the app gets suspended. Also is it correct to say if you call `endBackgroundTask` then then expirationHandler will get cancelled? i.e. something _similar_ to `expirationHandlerWorkItem.cancel()` will get called. – mfaani Feb 18 '22 at 20:19
  • 1
    If you end the background task, your expiration handler will not be called. So, in that way, it is analogous to canceling a work item, though the details of the implementation are not really relevant. – Rob Feb 18 '22 at 20:32