6

I have the current function which works. I'm using it with completion handler:

func getTokenBalances(completion: @escaping (Bool) -> Void) {
    guard let url = URL(string: "someApiUrlFromLostandFound") else {
        print("Invalid URL")
        completion(false)
        return
    }
    
    AF.request(url, method: .get).validate().responseData(completionHandler: { data in
        do {
            guard let data = data.data else {
                print("Response Error:", data.error as Any)
                completion(false)
                return
            }
            
            let apiJsonData = try JSONDecoder().decode(TokenBalanceClassAModel.self, from: data)
            DispatchQueue.main.async {
                self.getTokenBalancesModel = apiJsonData.data.items
                completion(true)
            }
        } catch {
            print("ERROR:", error)
            completion(false)
        }
    })
}

How can I convert it to the new async/await functionality of swift 5.5?

This is what I've tried:

func getTokenBalances3() async {
    let url = URL(string: "someApiUrlFromLostandFound")

    let apiRequest = await withCheckedContinuation { continuation in
        AF.request(url!, method: .get).validate().responseData { apiRequest in
            continuation.resume(returning: apiRequest)
        }
    }
    
    
    let task1 = Task {
        do {
            // Decoder is not asynchronous
            let apiJsonData = try JSONDecoder().decode(SupportedChainsClassAModel.self, from: apiRequest.data!)
//            Working data ->    print(String(apiJsonData.data.items[0].chain_id!))
        } catch {
            print("ERROR:", error)
        }
    }
        
    let result1 = await task1.value
    
    print(result1)  // values are not printed
}

But I'm not getting the value at the end on the print statement.

I'm kind of lost in the process, I'd like to convert my old functions, with this example it would help a lot.

EDIT:

The Answer below works, but I found my own solution while the Alamofire team implements async:

func getSupportedChains() async throws -> [AllChainsItemsClassAModel] {
    var allChains: [AllChainsItemsClassAModel] = [AllChainsItemsClassAModel]()
    let url = URL(string: covalentHqUrlConnectionsClassA.getCovalenHqAllChainsUrl())

    let apiRequest = await withCheckedContinuation { continuation in
        AF.request(url!, method: .get).validate().responseData { apiRequest in
            continuation.resume(returning: apiRequest)
        }
    }

    do {
        let data = try JSONDecoder().decode(AllChainsClassAModel.self, from: apiRequest.data!)
        allChains = data.data.items
    } catch {
        print("error")
    }

    return allChains
}
matt
  • 515,959
  • 87
  • 875
  • 1,141
Arturo
  • 3,254
  • 2
  • 22
  • 61
  • 1
    Alamofire has started concurrency work on the `feature/async-handlers` branch. You can use that branch or copy the wrapper work into your own code. – Jon Shier Aug 09 '21 at 19:19
  • 1
    I'll wait until official release @JonShier thanks for the heads up, I'm looking forward to it :) – Arturo Aug 09 '21 at 19:53

3 Answers3

12

First of all, your structure is wrong. Do not start with your original code and wrap all of it in the continuation block. Just make a version of AF.request itself that's wrapped in a continuation block. For example, the JSON decoding is not something that should be part of what's being wrapped; it is what comes after the result of networking returns to you — it is the reason why you want to turn AF.request into an async function to begin with.

Second, as the error message tells you, resolve the generic, either by the returning into an explicit return type, or by stating the type as part of the continuation declaration.

So, for example, what I would do is just minimally wrap AF.request in an async throws function, where if we get the data we return it and if we get an error we throw it:

func afRequest(url:URL) async throws -> Data {
    try await withUnsafeThrowingContinuation { continuation in
        AF.request(url, method: .get).validate().responseData { response in
            if let data = response.data {
                continuation.resume(returning: data)
                return
            }
            if let err = response.error {
                continuation.resume(throwing: err)
                return
            }
            fatalError("should not get here")
        }
    }
}

You'll notice that I didn't need to resolve the generic continuation type because I've declared the function's return type. (This is why I pointed you to my explanation and example in my online tutorial on this topic; did you read it?)

Okay, so the point is, now it is trivial to call that function within the async/await world. A possible basic structure is:

func getTokenBalances3() async {
    let url = // ...
    do {
        let data = try await self.afRequest(url:url)
        print(data)
        // we've got data! okay, so
        // do something with the data, like decode it
        // if you declare this method as returning the decoded value,
        // you could return it
    } catch {
        print(error)
        // we've got an error! okay, so
        // do something with the error, like print it
        // if you declare this method as throwing,
        // you could rethrow it
    }
}

Finally I should add that all of this effort is probably wasted anyway, because I would expect the Alamofire people to be along with their own async versions of all their asynchronous methods, any time now.

matt
  • 515,959
  • 87
  • 875
  • 1,141
  • I see what you are saying, now it's working somehow but still having problems returning the value at the end of the function, oh man my head is fried... been with this since this morning, I think I'm close – Arturo Aug 07 '21 at 21:32
  • Do I need to have another await on the `JSONDecoder`? Because the value is in a try catch and is outside of scope to return it – Arturo Aug 07 '21 at 21:35
  • I updated the code on the question at the bottom, why the print statement doesn't get the data? – Arturo Aug 07 '21 at 21:42
  • What on earth is the Task for? I just told you, the decoder is not asynchronous. – matt Aug 07 '21 at 23:09
  • I've added a possible implementation architecture to my answer. – matt Aug 07 '21 at 23:23
  • Woops and I just see this now, I figured it out and put the puzzles together, I feel kind of dumb for spending so much time on this issue but at least I figured it out. I'm reading your answer now, I think I'll mix your implementation with mine for best results, when I'm done I'll add it above in the question to see if you see any flaws – Arturo Aug 08 '21 at 00:26
  • Can you please add a +1 to the question... this is incredible... now Stackoverflow doesn't let me ask anymore with this account... waoh... – Arturo Aug 08 '21 at 00:30
  • Do not ask people for votes. Try to benefit from the situation as a learning experience (i.e. learn to ask questions that are more suitable to Stack Overflow). – matt Aug 08 '21 at 01:31
5

Personally I think swallowing errors inside a network call is a bad idea, the UI should receive all errors and make the choice accordingly.

Here is an example of short wrapper around responseDecodable, that produces an async response.

public extension DataRequest {

    @discardableResult
    func asyncDecodable<T: Decodable>(of type: T.Type = T.self,
                                      queue: DispatchQueue = .main,
                                      dataPreprocessor: DataPreprocessor = DecodableResponseSerializer<T>.defaultDataPreprocessor,
                                      decoder: DataDecoder = JSONDecoder(),
                                      emptyResponseCodes: Set<Int> = DecodableResponseSerializer<T>.defaultEmptyResponseCodes,
                                      emptyRequestMethods: Set<HTTPMethod> = DecodableResponseSerializer<T>.defaultEmptyRequestMethods) async throws -> T {

        return try await withCheckedThrowingContinuation({ continuation in

            self.responseDecodable(of: type, queue: queue, dataPreprocessor: dataPreprocessor, decoder: decoder, emptyResponseCodes: emptyResponseCodes, emptyRequestMethods: emptyRequestMethods) { response in

                switch response.result {
                case .success(let decodedResponse):
                    continuation.resume(returning: decodedResponse)
                case .failure(let error):
                    continuation.resume(throwing: error)
                }
            }
        })
    }
}
John
  • 61
  • 1
  • 2
0

This is a mix between my Answer and the one that matt provided. There will probably be an easier and cleaner implementation once the Alamofire team implements async but at least for now I'm out of the call backs hell...

func afRequest(url: URL) async throws -> Data {
    try await withUnsafeThrowingContinuation { continuation in
        AF.request(url, method: .get).validate().responseData { response in
            if let data = response.data {
                continuation.resume(returning: data)
                return
            }
            if let err = response.error {
                continuation.resume(throwing: err)
                return
            }
            fatalError("Error while doing Alamofire url request")
        }
    }
}


func getSupportedChains() async -> [AllChainsItemsClassAModel] {
    var allChains: [AllChainsItemsClassAModel] = [AllChainsItemsClassAModel]()
    let url = URL(string: covalentHqUrlConnectionsClassA.getCovalenHqAllChainsUrl())

    do {
        let undecodedData = try await self.afRequest(url: url!)
        let decodedData = try JSONDecoder().decode(AllChainsClassAModel.self, from: undecodedData)
        allChains = decodedData.data.items
    } catch {
        print(error)
    }

    return allChains
}
Arturo
  • 3,254
  • 2
  • 22
  • 61
  • Actually that's basically identical to what I said. And it isn't as good, because it makes no sense to return an empty array when there's an error. – matt Aug 08 '21 at 01:29
  • So how do I return it then? In your answer is not included (I didn't mention it for simplicity) @matt – Arturo Aug 08 '21 at 01:40
  • The way to signal failure is to fail. Did you read my answer? (It seems not, just as you didn't read the link I gave you.) "If you declare this method as throwing, you could rethrow [the error].)" – matt Aug 08 '21 at 01:59