1

So I'm having an issue running an API call whose response includes another API call.

Here's the first function:

class APICaller{

    weak var delegate:APIDelegate?

    func getCharacter(x:Int){
        let character = CharacterModel()
        let url = URL(string: "https://swapi.co/api/people/\(x)/")
        let task = URLSession.shared.dataTask(with: url!) { (data, response, error) in
            if error != nil{
                print("Error downloading character information. Empty character returned.")
            } else {
                if let content = data {

                    do{
                        let charJSON = try JSONSerialization.jsonObject(with: content, options: JSONSerialization.ReadingOptions.mutableContainers) as? [String: Any]
                        character.name = (charJSON?["name"] as? String)?.description ?? ""
                        character.height = Int((charJSON?["height"] as? String)?.description ?? "0") ?? 0
                        character.mass = Int((charJSON?["mass"] as? String)?.description ?? "0") ?? 0
                        character.hairColor = (charJSON?["hair_color"] as? String)?.description ?? ""
                        character.skinColor = (charJSON?["skin_color"] as? String)?.description ?? ""
                        character.eyeColor = (charJSON?["eye_color"] as? String)?.description ?? ""
                        character.birthYear = (charJSON?["birth_year"] as? String)?.description ?? ""
                        character.gender = (charJSON?["gender"] as? String)?.description ?? ""
                        character.homeWorld = self.getPlanet(uri: (charJSON?["homeworld"] as? String)?.description ?? "")
//The homeward part of the response is another URL and as such requires another API Call to get anything meaningful
                            DispatchQueue.main.async {
                                self.delegate?.didGetStarWarsCharacter(characterData:character)
                            }
                        }catch{
                            print("Error downloading character information. Empty or incomplete character returned")
                        }
                    }
                }
            }
            task.resume()
        }
    private func getPlanet(uri:String)->String{
        if uri == ""{
            return uri // return empty string if the original call doesn't get anything.
        }
        var result = ""
        let url = URL(string:uri)
        let task = URLSession.shared.dataTask(with: url!){(data,response,error)->Void in
            if error != nil{
                result = "No Planet Found"
            }else{
                if let planet = data{
                    do{
                        let planetJSON = try JSONSerialization.jsonObject(with: planet, options: JSONSerialization.ReadingOptions.mutableContainers) as? [String:Any]
                        //print(planetJSON?["name"] as? String ?? "No Planet")
                        result = (planetJSON?["name"] as? String)?.description ?? "No Planet Found"
                    }catch{
                        result = "No Planet Found"
                    }
                }
            }
        }// end of task, result is lost due to multithreading
        task.resume()
        return result
        }
    }

So I understand that running the task for getPlanet happens on another thread, and this method returns before before the task can finish running. As such when the delegate gets the CharacterModel, its homeWorld parameter is empty.

For example, if I were to call print(character.homeWorld) after the getPlanet function runs I would receive an empty String.

What I can't figure out is a good solution to this problem.

Joey Nash
  • 35
  • 6
  • Add an `escaping closure` to your `getPlanet(uri:String)` method to return the result back to the caller. Call that closure when you successfully receive data from server. https://stackoverflow.com/a/45976392/5912335 – badhanganesh Nov 12 '17 at 14:41
  • So you're saying within the task I should have a closure annotated as `@escaping` that sends the result back to the object of class API Caller? – Joey Nash Nov 12 '17 at 16:45
  • Exactly. And Remove your `getPlanet` method’s return value (`-> String`)from the method definition. That’s not needed. – badhanganesh Nov 12 '17 at 16:52
  • Ok. So removing the return, probably setting up a quick String instance variable, and after getting the String from the call, send it to that instance variable in an escaping closure. Only thing left after that is how to make sure that value is added to the character's homeWorld parameter before the delegate's `didGetStarWarsCharacter` is called. – Joey Nash Nov 12 '17 at 17:08
  • That's easy. When calling `getPlanet` method, you will be declaring the closure associated with it. So when you call that `escaping closure` after the response from service, that associated closure will get executed which passes the result string. You use it to set the `homeWorld` property. This answer is all you need to understand how closures can be used to return data after a service call: https://stackoverflow.com/a/39504347/5912335 – badhanganesh Nov 12 '17 at 17:15
  • Between you and the guy who posted it as an answer, I got it done. Thanks. – Joey Nash Nov 12 '17 at 17:59
  • That’s cool. @JoeyNash – badhanganesh Nov 12 '17 at 18:00

1 Answers1

1

getPlanet is performing an asynchronous call. The result instance will not hold the received data. Instead of that use a completion block in the getPlanet and on receiving the data invoke this completion block. Something like this. Do read on closures.

class APICaller{

    weak var delegate:APIDelegate?

    func getCharacter(x:Int){
        let character = CharacterModel()
        let url = URL(string: "https://swapi.co/api/people/\(x)/")
        let task = URLSession.shared.dataTask(with: url!) { (data, response, error) in
            if error != nil{
                print("Error downloading character information. Empty character returned.")
            } else {
                if let content = data {

                    do{
                        let charJSON = try JSONSerialization.jsonObject(with: content, options: JSONSerialization.ReadingOptions.mutableContainers) as? [String: Any]
                        character.name = (charJSON?["name"] as? String)?.description ?? ""
                        character.height = Int((charJSON?["height"] as? String)?.description ?? "0") ?? 0
                        character.mass = Int((charJSON?["mass"] as? String)?.description ?? "0") ?? 0
                        character.hairColor = (charJSON?["hair_color"] as? String)?.description ?? ""
                        character.skinColor = (charJSON?["skin_color"] as? String)?.description ?? ""
                        character.eyeColor = (charJSON?["eye_color"] as? String)?.description ?? ""
                        character.birthYear = (charJSON?["birth_year"] as? String)?.description ?? ""
                        character.gender = (charJSON?["gender"] as? String)?.description ?? ""
                        self.getPlanet(uri: (charJSON?["homeworld"] as? String)?.description ?? "", completion:
                            { (result:String) in
                                character.homeWorld = result
                                DispatchQueue.main.async {
                                    self.delegate?.didGetStarWarsCharacter(characterData:character)
                                }
                        }
                        )
                        //The homeward part of the response is another URL and as such requires another API Call to get anything meaningful

                    }catch{
                        print("Error downloading character information. Empty or incomplete character returned")
                    }
                }
            }
        }
        task.resume()
    }
    private func getPlanet(uri:String, completion:@escaping (_ response:String)->Void){
        if uri == ""{
            completion(uri) // return empty string if the original call doesn't get anything.
        }
        var result = ""
        let url = URL(string:uri)
        let task = URLSession.shared.dataTask(with: url!){(data,response,error)->Void in
            if error != nil{
                result = "No Planet Found"
            }else{
                if let planet = data{
                    do{
                        let planetJSON = try JSONSerialization.jsonObject(with: planet, options: JSONSerialization.ReadingOptions.mutableContainers) as? [String:Any]
                        //print(planetJSON?["name"] as? String ?? "No Planet")
                        result = (planetJSON?["name"] as? String)?.description ?? "No Planet Found"
                    }catch{
                        result = "No Planet Found"
                    }
                    completion(result)
                }
            }
        }// end of task, result is lost due to multithreading
        task.resume()
    }
}
Arun Balakrishnan
  • 1,462
  • 1
  • 12
  • 24
  • Thank you. So if my understanding is correct, I pass a closure by the name "completion" that takes a String argument as a parameter to the function. The @escaping annotation allows that closure to escape the method and access the argument from within the getCharacter method. Then in my getPlanet function I make sure to call the closure anytime that I would need to access the result. That's brilliant. – Joey Nash Nov 12 '17 at 18:01
  • Yes. Welcome to Swift! – Arun Balakrishnan Nov 12 '17 at 18:07