Home > Back-end >  Accessing Google API data from within 3 async callbacks and a function in SwiftUI
Accessing Google API data from within 3 async callbacks and a function in SwiftUI

Time:08-22

I know this question is asked a lot, but I can't figure out how to apply any answers to my program. Sorry in advance this async stuff makes absolutely zero sense to me.

Basically, I have a button in SwiftUI that, when pressed, calls a function that makes two API calls to Google Sheets using Alamofire and GoogleSignIn.

Button("Search") {
    if fullName != "" {
        print(SheetsAPI.nameSearch(name: fullName, user: vm.getUser()) ?? "Error")
    }
}

This function should return the values of some cells on success or nil on an error. However, it only ever prints out "Error". Here is the function code.

static func nameSearch<S: StringProtocol>(name: S, advisory: S = "", user: GIDGoogleUser?) -> [String]? {
    let name = String(name)
    let advisory = String(advisory)
    let writeRange = "'App Control'!A2:C2"
    let readRange = "'App Control'!A4:V4"
    
    // This function can only ever run when user is logged in, ! should be fine?
    let user = user!
    
    let parameters: [String: Any] = [
        "range": writeRange,
        "values": [
            [
                name,
                nil,
                advisory
            ]
        ]
    ]
    
    // What I want to be returned
    var data: [String]?
    
    // Google Identity said use this wrapper so that the OAuth tokens refresh
    user.authentication.do { authentication, error in
        guard error == nil else { return }
        guard let authentication = authentication else { return }
        
        // Get the access token to attach it to a REST or gRPC request.
        let token = authentication.accessToken
        let headers: HTTPHeaders = ["Authorization": "Bearer \(token)"]
        
        AF.request("url", method: .put, parameters: parameters, encoding: JSONEncoding.default, headers: headers).responseString { response in
            
            switch response.result {
            case .success:

                // I assume there is a better way to make two API calls...
                AF.request("anotherURL", headers: headers).responseDecodable(of: NameResponseModel.self) { response2 in
                    
                    switch response2.result {
                    case .success:
                        guard let responseData = response2.value else { return }
                        data = responseData.values[0]
                        // print(responseData.values[0]) works fine
                        
                    case .failure:
                        print(response2.error ?? "Unknown error.")
                        data = nil
                        
                    }
                }
                
            case .failure:
                print(response.error ?? "Unknown error.")
                data = nil
            }
        }
    }
    // Always returns nil, "Unknown error." never printed
    return data
}

The model struct for my second AF request:

struct NameResponseModel: Decodable { let values: [[String]] }

An example API response for the second AF request:

{
  "range": "'App Control'!A4:V4",
  "majorDimension": "ROWS",
  "values": [
    [
      "Bob Jones",
      "A1234",
      "Cathy Jones",
      "1234 N. Street St. City, State 12345"
    ]
  ]
}

I saw stuff about your own callback function as a function parameter (or something along those lines) to handle this, but I was completely lost. I also looked at Swift async/await, but I don't know how that works with callback functions. Xcode had the option to refactor user.authentication.do { authentication, error in to let authentication = try await user.authentication.do(), but it threw a missing parameter error (the closure it previously had).

EDIT: user.authentication.do also returns void--another reason the refactor didn't work (I think).

There is probably a much more elegant way to do all of this so excuse the possibly atrocious way I did it.

Here is the link to Google Identity Wrapper info.

Thanks in advance for your help.

CodePudding user response:

Solved my own problem.

It appears (according to Apple's async/await intro video) that when you have an unsupported callback that you need to run asynchronously, you wrap it in something called a Continuation, which allows you to manually resume the function on the thread, whether throwing or returning.

So using that code allows you to run the Google Identity token refresh with async/await.

private static func auth(_ user: GIDGoogleUser) async throws -> GIDAuthentication? {
    typealias AuthContinuation = CheckedContinuation<GIDAuthentication?, Error>
        
    return try await withCheckedThrowingContinuation { (continuation: AuthContinuation) in
        user.authentication.do { authentication, error in
            if let error = error {
                continuation.resume(throwing: error)
            } else {
                continuation.resume(returning: authentication)
            }
        }
    }
}

static func search(user: GIDGoogleUser) async throws {

    // some code

    guard let authentication = try await auth(user) else { ... }

    // some code
}

I then ran that before using Alamofire's built-in async/await functionality for each request (here's one).

let dataTask = AF.request(...).serializingDecodable(NameResponseModel.self)
let response = try await dataTask.value

return response.values[0]
  • Related