Search code examples
swiftinfluxdb

How to correctly await a Swift callback closure result


In the past when I needed to wait for a Swift callback closure (in this case to the InfluxDB Swift API) to produce a result I used a semaphore to signal the completion.

    func getChargeList(from: Int, until: Int) async -> [Charge] {
        let flux =  """
                    from(bucket: "\(self.bucket)")
                        ...
                    """
        let queryTask = Task { () -> [Charge] in
            **let s = DispatchSemaphore(value: 0)**
            var chargeSessions: [Charge] = []
            self.client.queryAPI.query(query: flux) { response, error in
                if let error = error {
                    print("getChargeList error: \(error)")
                }
                if let response = response {
                    do {
                        ...
                }
                **s.signal()**
            }
            **s.wait()**
            return chargeSessions
        }
        return await queryTask.value
    }

As of Swift 5.7 I get a warning

Instance method 'wait' is unavailable from asynchronous contexts; Await a Task handle instead; this is an error in Swift 6

so time to look at solutions.

Being new to Swift and asynchronous programming I have not come up to a solution to the InfluxDB query API returning immediately and then suspending execution until the query results are returned using the trailing closure. Hopefully just missing something something simple due to my lack of experience so any comments will be appreciated.

I have opened an issue with the InfluxDB Swift API repo to consider using new async/await standard but a workaround or solution ahead of an updated library would be useful to have.


Solution

  • You don't need task. Instead, do this:

    1. Wrap your query in function with callback:
    func runQuery(from: Int, until: Int, callback: (Result<[Charge], Error>) -> ()) {
         let flux =  """
                        from(bucket: "\(self.bucket)")
                            ...
                        """
         self.client.queryAPI.query(query: flux) { response, error in
             if let error = error {
                 callback(.failure(error))
                 return
             }
             if let response = response {
                 let chargeSessions = ...
                 callback(.success(chargeSessions))
             }
        }
    }
    
    1. Create an await/async wrapper for this function:
    func getChargeList(from: Int, until: Int) async -> Result<[Charge], Error> {
        await withCheckedContinuation { continuation in
            runQuery(from: from, until: until) { result in
                continuation.resume(returning: result)
            }
        }
    }
    

    The use of Result is optional of course, but it allows to conveniently pack both successful and failed cases.

    Also you could fit it into 1 function, but then you have 4 level of braces - not a convenient code to read.