swiftgrand-central-dispatchcompletionhandlerswift-concurrency

How to wait until data from network call comes and only then return value of a function #Swift


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()
}

Solution

  • 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.