Swift: Waiting for dispatch_async to finish on main thread

113 Views Asked by At

I feel like there is an obvious solution to this that is on the tip of my brain, but I can't seem to figure it out. I am using the FirebaseAuth library, so I can't edit it (or I don't want to go down that path). The function getIDTokenForcingRefresh() uses dispatch_async. This would be a lot easier if it used the async/await functionality from Swift 5.5, but I have to rely on solutions for dispatch_async. I need to grab the output to run a Firebase function request using the token. The following obviously doesn't work because the function will return before getIDTokenForcingRefresh() is finished. I don't care if the main thread is blocked because the user can't proceed in the app until this is done.

    var userToken: String {

        print("FIREBASE: Getting User Token")
        var token = ""
        Auth.auth().currentUser?.getIDTokenForcingRefresh(true) { idToken, error in
            if let error = error {
                print("FIREBASE: There was an error getting the user token")
                return
            }
            print("FIREBASE: Got user token: \(idToken ?? "NONE")")
            token = idToken ?? ""
        }
        print("FIREBASE: Token: \(token)")
        return token
    }
2

There are 2 best solutions below

2
vadian On BEST ANSWER

Even with a completion handler method like getIDTokenForcingRefresh you can take advantage of async/await by wrapping the asynchronous function with completion handler in a Continuation. The error handling is for free

func getUserToken() async throws -> String {
    print("FIREBASE: Getting User Token")
    return withCheckedThrowingContinuation { continuation in
        Auth.auth().currentUser?.getIDTokenForcingRefresh(true) { idToken, error in
            if let error {
                print("FIREBASE: There was an error getting the user token: \(error.localizedDescription)")
                continuation.resume(throwing: error)
            } else {
                print("FIREBASE: Got user token: \(idToken ?? "NONE")")
                continuation.resume(returning: idToken!)
            }
        }
    }
}

And use it

Task {
    do {
        let token = try await getUserToken()
    } catch {
        print(error)
    }
}

Doesn't Firebase support async/await meanwhile?

1
tharris On

EDIT: Thanks to vadian's comment above and HangerRash's suggestion, an async computed property is better:

extension Auth {
    var currentUserToken: Task<String, Error> {
        Task { @MainActor in
            do {
                let idToken = try await currentUser?.getIDTokenResult()
                print("FIREBASE: Got user token: \(idToken?.token ?? "NONE")")
                return idToken?.token ?? ""
            } catch {
                print("FIREBASE: There was an error getting the user token")
                throw error
            }
        }
    }
}
        var token = ""
        do {
            token = try await Auth.auth().currentUserToken.value
            //print("FIREBASE: Token: \(token)")
            // Proceed with the code that depends on the token
        } catch {
            print("FIREBASE: Error retrieving user token: \(error)")
            // Handle the error
        }
        
        print("FIREBASE: Token = \(token)")

Old, non-optimal semaphore way

I was able to figure this out using semaphore:

    func getUserToken(completion: @escaping (String?) -> Void) {
        print("FIREBASE: Getting User Token")
        Auth.auth().currentUser?.getIDTokenForcingRefresh(true) { idToken, error in
            if let error = error {
                print("FIREBASE: There was an error getting the user token: \(error.localizedDescription)")
                completion(nil)
            } else {
                print("FIREBASE: Got user token: \(idToken ?? "NONE")")
                completion(idToken)
            }
        }
    }
    
    func getUserTokenBlocking() -> String {
        var userToken: String = ""
        let semaphore = DispatchSemaphore(value: 0)
        
        GPTClient.getUserToken { token in
            userToken = token ?? ""
            semaphore.signal()
        }
        
        semaphore.wait()
        return userToken
    }

Now when I want to grab the token I use:

let token = getUserTokenBlocking()

I obviously need to do some error handling, and this is only useful when you WANT to block the main thread, which usually isn't the right answer but works for me in this situation.