1

I've made two functions which call two different rest APIs with an HTTP GET request. I've made a class call "ExchangeObject" which encapsulates the data retrieved from the APIs into an object. In each call, I append an object of class ExchangeObject to an array called exchangesArray.

One property of the ExchangeObject class is called "price" and it's an int value. In viewDidLoad, I call another function that calculates the average of the prices of the two objects in the array. However, the array is always empty! I don't know why. I think it's because I'm making asynchronous calls but I don't know how to fix it. However, my tableview isn't empty. It displays the two prices of the objects in the array.

class ViewController: UIViewController, UITableViewDataSource{


     @IBOutlet weak var exchangesTableView: UITableView!

     var exchangesArray = [ExchangeObject]()


     override func viewDidLoad() {
         super.viewDidLoad()
         exchangesTableView.dataSource = self

         gemini()
         bitfinex()
         average()

     }


     func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int{

         return exchangesArray.count

     }



     func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell{

         let cell = exchangesTableView.dequeueReusableCell(withIdentifier: "cell")
         cell?.textLabel?.text = exchangesArray[indexPath.row].name
         cell?.detailTextLabel?.text = exchangesArray[indexPath.row].price

         return cell!

     }


     func gemini(){
         let url = "https://api.gemini.com/v1/pubticker/ethusd"
         var request = URLRequest(url: URL(string: url)!)
         request.httpMethod = "GET"

         let configuration = URLSessionConfiguration.default
         let session = URLSession(configuration: configuration, delegate: nil, delegateQueue: OperationQueue.main)

         let task = session.dataTask(with: request) { (data, response, error) in

             if(error != nil){
                 print("Error")
             }
             else{
                 do{
                     let fetchedData = try JSONSerialization.jsonObject(with: data!, options: .allowFragments) as AnyObject
                     let price = fetchedData["last"] as! String
                     self.exchangesArray.append(ExchangeObject(name: "Gemini", price: price))
                     DispatchQueue.main.async {
                         self.exchangesTableView.reloadData()
                     }

                 }
                 catch{
                     print("Error")
                 }
             }
         }
         task.resume()
     }


     func bitfinex(){
         let url = "https://api.bitfinex.com/v2/ticker/tETHUSD"
         var request = URLRequest(url: URL(string: url)!)
         request.httpMethod = "GET"

         let configuration = URLSessionConfiguration.default
         let session = URLSession(configuration: configuration, delegate: nil, delegateQueue: OperationQueue.main)

         let task = session.dataTask(with: request) { (data, response, error) in

             if(error != nil){
                 print("Error")
             }
             else{
                 do{
                     let fetchedData = try JSONSerialization.jsonObject(with: data!, options: .allowFragments) as! NSArray
                     let priceInt = fetchedData[6]
                     let price = String(describing: priceInt)
                     self.exchangesArray.append(ExchangeObject(name: "Bitfinex", price: price))
                     DispatchQueue.main.async {
                         self.exchangesTableView.reloadData()
                     }
                 }
                 catch{
                     print("Error")
                 }
             }
         }
         task.resume()
     }




     func average() -> Void{

         var total: Int = 0
         var average: Int

         for eth in self.exchangesArray{

             total = total + Int(eth.price)!

         }

         average = total/self.exchangesArray.count
         print("The average is: \(average)")
      }



 }


 class ExchangeObject{

     var name: String
     var price: String

     init(name: String, price: String){
         self.name = name
         self.price = price
     }

 }
user3341399
  • 81
  • 2
  • 4
  • 8
  • 1
    If you've confirmed you truly are receiving valid data, then Yes, it's likely due to the asynchronous behavior . Try moving your appends to the main queue as well to see if they are updated in a more timely fashion. – Mozahler Aug 19 '17 at 00:51

1 Answers1

3

You misunderstand how async methods work. Your methods gemini() and bitfinex() are both asynchronous. They both start data tasks on a background thread, and return immediately, before the data tasks have even begun to execute. At some later time the data task completes and the completion method of your data task gets called. By then, though, your average() method has already been called.

You should rewrite both of those methods to take completion handlers:

func bitfinex(completion: () -> void) {
    let url = "https://api.bitfinex.com/v2/ticker/tETHUSD"
    var request = URLRequest(url: URL(string: url)!)
    request.httpMethod = "GET"

    let configuration = URLSessionConfiguration.default
    let session = URLSession(configuration: configuration, delegate: nil, delegateQueue: OperationQueue.main)

    let task = session.dataTask(with: request) { (data, response, error) in
        if(error != nil) {
             print("Error")
        }
        else {
             do {
                 let fetchedData = try JSONSerialization.jsonObject(with: data!, options: .allowFragments) as! NSArray
                 let priceInt = fetchedData[6]
                 let price = String(describing: priceInt)
                 self.exchangesArray.append(ExchangeObject(name: "Bitfinex", price: price))
                 DispatchQueue.main.async {
                     completion()
                 }
             }
             catch {
                 print("Error")
             }
        }
    }
    task.resume()
}


Refactor the gemini() method the same way, and then call them like this:

gemini() {
  self.bitfinex() {
     self.tableView.reloadData()
     self.average()
  }
}

What you've done is to set up the gemini() and bitfinex() methods so that they each take a block of code as a parameter. They do their thing, and then run that code once the async work is done.

The completion block for the gemini() function calls bitfinex(), and the completion block for bitfinex() reloads the table view and then calls your average method.

I left out some details like what to do if the data task returns an error, but that should be enough to get you started.

Also note that it looks like your two methods are pretty much identical other than the URLs they point to and the names they use to create their output ExchangeObjects. It would make more sense to create a general purpose function:

func getStock(symbol: String, remoteURL: String, completion: () -> void)

And then call that twice with different parameters:

getStock(symbol: "gemini", 
    remoteURL: "https://api.gemini.com/v1/pubticker/ethusd") {
  self.getStock(symbol: "Bitfinex", remoteURL: "https://api.bitfinex.com/v2/ticker/tETHUSD") {
    self.tableView.reloadData()
    self.average()
  }
}
Duncan C
  • 128,072
  • 22
  • 173
  • 272
  • Thank you so much for this wonderful explanation it has helped tremendously. I really do appreciate you taking the time to write all this out. – user3341399 Aug 19 '17 at 04:28
  • what if you had more than 2 API's. What if I wanted to call many more APIs. Would the pattern be to continue calling the functions within the function that came before it's completion handler? – user3341399 Aug 19 '17 at 22:15
  • Then you should look at a pattern called a Future. – Duncan C Aug 20 '17 at 02:00