Search code examples
swiftasync-awaitswift5

How do we wait for an async/await function run completely and then run the following line of code in Swift?


How do we wait for an async/await function and then run the following line of code in Swift?

await res.checkExistingUser(email: loginForm.email)
print("after that") // it doesn't wait for the above function to run completely

in the res.checkExistingUser() is used for calling API and mapping the model.

@MainActor
final class LoginViewModelImpl: LoginViewModel {
    @Published var hasError = false
    @Published private(set) var errorMesg = ""
    @Published private(set) var isFetching = false
    @Published private(set) var isLoggedIn = false
    
    @Published private(set) var isVerify = false
    
    func checkExistingUser(email: String) async {
        self.isFetching = true
        let res = UserServiceImpl()
        
// Call API
        res.checkExistingUser(email: email).responseDecodable(of: BasedResponse<UserExistModel>.self) { response in
            switch response.result {
            case .success(let apiListingResponse):
                print(apiListingResponse)

                if !apiListingResponse.success {
                    self.hasError = true
                    self.errorMesg = String(localized: String.LocalizationValue((apiListingResponse.error?.message)!) )
                    self.isFetching = false
                    break
                }
                
                if (apiListingResponse.payload?.message) != nil {
                    self.isVerify = false
                    self.hasError = true
                    self.errorMesg = String(localized: String.LocalizationValue("verify_email"))
                    self.isFetching = false
                    return
                }
            ...
        }
    }

Solution

  • I would suggest watching WWDC 2021 video Swift concurrency: Update a sample app which is a nice, practical demonstration of refactoring old completion-handler-based code to async-await.

    For example, as discussed in that video, you can have the compiler create an async “wrapper” implementation of responseDecodable. E.g., control-click (or right-click) on the responseDecodable implementation and choose “Refactor” » “Add Async Wrapper”, and it will produce an async rendition that calls your completion handler rendition.

    enter image description here

    Now, you didn't share the function signature of responseDecodable, but for illustrative purposes, let us assume it was the following:

    func responseDecodable<T: Decodable>(of dataType: T.Type = T.self, completion: @escaping (BasedResponse<T>) -> Void) {
        …
    }
    

    Then you can add a wrapper implementation:

    func responseDecodable<T: Decodable>(of dataType: T.Type = T.self) async -> BasedResponse<T> {
        await withCheckedContinuation { continuation in
            responseDecodable(of: dataType) { result in
                continuation.resume(returning: result)
            }
        }
    }
    

    Clearly, if your responseDecodable was different, just modify that wrapper implementation accordingly. (Or let “Refactor” » “Add Async Wrapper” feature do this for you.) And if responseDecodable is from some library you cannot edit, do not worry: Just put your async rendition in an extension of its original type in your own codebase.

    Anyway, you can now replace the incorrect checkExistingUser:

    func checkExistingUser(email: String) async {
        self.isFetching = true
        let res = UserServiceImpl()
        
        res.checkExistingUser(email: email).responseDecodable(of: BasedResponse<UserExistModel>.self) { response in
            switch response.result { … }
        }
    }
    

    With a rendition that will await your new async implementation of responseDecodable:

    func checkExistingUser(email: String) async {
        self.isFetching = true
        let res = UserServiceImpl()
        
        let response = await res.checkExistingUser(email: email).responseDecodable(of: BasedResponse<UserExistModel>.self)
    
        switch response.result { … }
    }
    

    You now have an implementation of checkExistingUser which will properly await the result.


    Frankly, this process of wrapping a completion handler rendition with an async rendition is merely an interim solution. You really want to eventually retire all of your completion handler implementations, altogether. Personally, as advised in that video, the pattern I've used in my projects is as follows:

    • add an async wrapper rendition;
    • add a deprecation warning to the completion handler rendition;
    • fix all the calling points that are using the completion handler rendition to follow async-await patterns; and, when all done with that
    • go back and
      • refactor the completion handler rendition to follow async-await, itself
      • retire the wrapper rendition I wrote earlier

    Note, I did not have a reproducible example of your issue, so I made some assumptions in the above. So, do not get bogged down in the details. The idea is to wrap/refactor your completion handler method with an async one that uses withCheckedContinuation (or withThrowingCheckedContinuation), and now you can await a function that formerly had a completion handler.