Home > Back-end >  How to wait until data from network call comes and only then return value of a function #Swift
How to wait until data from network call comes and only then return value of a function #Swift

Time:12-17

I have a service class that makes an api call and stores data into its property. Then my interactor class have a method where I want to make service class api call and when data will be stored - return it. I tried myself to handle this with completion handler and dispatch group, but (I suppose I just missing something) this didn't work. I would be very appreciated if you help me to deal with this problem. Thanks in advance!

Service class:

class PunkApiService{

var beers = [Beer]()

func loadList(at page: Int){
    //MARK: - Checks is URL is valid   pagination
    guard let url = URL(string: "https://api.punkapi.com/v2/beers?page=\(page)&per_page=25") else {
        print("Invalid URL")
        return
    }
    //MARK: - Creating URLSession DataTask
    let task = URLSession.shared.dataTask(with: url){ data, response, error in
        //MARK: - Handling no erros came
        guard error == nil else {
            print(error!)
            return
        }
        //MARK: - Handling data came
        guard let data = data else{
            print("Failed to load data")
            return
        }
        do{
            let beers = try JSONDecoder().decode([Beer].self, from: data)
            self.beers.append(contentsOf: beers)
        }
        catch{
            print("Failed to decode data")
        }
    }
    task.resume()
}

And Interactor class(without completion handler or dispatch group):

class BeersListInteractor:BeersListInteractorProtocol{
private var favoriteBeers = FavoriteBeers()
private var service  = PunkApiService()
//MARK: - Load list of Beers
func loadList(at page: Int) -> [Beer]{
    service.loadList(at: page)
    return service.beers
}

Added: my attempt with completion handler

    var beers: [Beer]                                          
func loadList(at page: Int, completion: ()->()){
    service.loadList(at: page)
    completion()
    
}

func completion(){
    beers.append(contentsOf: service.beers)
}

loadList(at: 1) {
    completion()
}

CodePudding user response:

This is what async/await pattern is for, described here. In your case both loadList functions are async, and the second one awaits for the first one:

class PunkApiService {
    func loadList(at page: Int) async { 
        // change function to await for task result 
        let (data, error) = try await URLSession.shared.data(from: url)
        let beers = try JSONDecoder().decode([Beer].self, from: data)
        ...
        return beers
    }
}

class BeersListInteractor: BeersListInteractorProtocol {
    func loadList(at page: Int) async -> [Beer]{
         let beers = await service.loadList(at: page)
         return service.beers
    }
}

See a good explanation here

CodePudding user response:

I think that you were on the right path when attempting to use a completion block, just didn't do it correctly.

func loadList(at page: Int, completion: @escaping ((Error?, Bool, [Beer]?) -> Void)) {
    //MARK: - Checks is URL is valid   pagination
    guard let url = URL(string: "https://api.punkapi.com/v2/beers?page=\(page)&per_page=25") else {
        print("Invalid URL")
        completion(nil, false, nil)
        return
    }
    //MARK: - Creating URLSession DataTask
    let task = URLSession.shared.dataTask(with: url){ data, response, error in
        //MARK: - Handling no erros came
        if let error = error {
            completion(error, false, nil)
            print(error!)
            return
        }
        //MARK: - Handling data came
        guard let data = data, let beers = try? JSONDecoder().decode([Beer].self, from: data) else { 
            completion(nil, false, nil)
            return
        }
        completion(nil, true, beers)
    }
    task.resume()
}

This is the loadList function, which now has a completion parameter that will have three parameters, respectively the optional Error, the Bool value representing success or failure of obtaining the data, and the actual [Beers] array, containing the data (if any was retrieved).

Here's how you would now call the function:

service.loadList(at: page) { error, success, beers in
    if let error = error {
        // Handle the error here
        return
    }

    if success, let beers = beers {
       // Data was correctly retrieved - and safely unwrapped for good measure, do what you need with it
       // Example:
       loader.stopLoading()
       self.datasource = beers
       self.tableView.reloadData()
    }
}

Bear in mind the fact that the completion is being executed asynchronously, without stopping the execution of the rest of your app. Also, you should decide wether you want to handle the error directly inside the loadList function or inside the closure, and possibly remove the Error parameter if you handle it inside the function. The same goes for the other parameters: you can decide to only have a closure that only has a [Beer] parameter and only call the closure if the data is correctly retrieved and converted.

  • Related