0

I am trying to build an app that displays Currency exchange rates using the Alpha Vantage API for iOS. I have built the functions but can't figure out how to access the exact json value which is "5. Exchange Rate".

Here is some of the code and the json data to help explain better:

The URL Built:

func USDtoEUR(_ completionHandler: @escaping (_ success: Bool, _ quotes: [String:AnyObject]?, _ error: String?) -> Void) {

    let urlString = "https://www.alphavantage.co/query?function=CURRENCY_EXCHANGE_RATE&from_currency=USD&to_currency=EUR&apikey=NP3M8LL62YJDO0YX"
        let session = URLSession.shared
        let url = URL(string: urlString)!

        let request = URLRequest(url: url)
        let task = session.dataTask(with: request, completionHandler: { data, response, error in
            if error != nil {
                completionHandler(false, nil, error!.localizedDescription)
            }
            else {
                do {
                    let result = try JSONSerialization.jsonObject(with: data!, options: JSONSerialization.ReadingOptions.allowFragments) as! NSDictionary
                    if let dictionary = result["Realtime Currency Exchange Rate"] as? [String:AnyObject]! {
                        completionHandler(true, dictionary, nil)
                    }
                    else {
                        completionHandler(false, nil, nil)
                    }
                } catch {
                    completionHandler(false, nil, "Unable to process retrieved data.")
                }
            }

        })
        task.resume()

    }

The Quotes in the view controller

func usdQUotesRequest() {

  USDClient().USDtoEUR() { success, newQuote, error in

    if success {
        self.usdtoeurquote = newQuote
        DispatchQueue.main.async {
            self.stopActivityIndicator()
            self.Refresh.isEnabled = true
        }

    } else {
        DispatchQueue.main.async {
            self.displayAlert("Unable to Retrieve Latest Conversion Rates", message: "\(error!)")
            self.stopActivityIndicator()
            self.Refresh.isEnabled = true

        }
    }
}

// The Quotes being showing after touching the USD button:

@IBAction func usdConversions(_ sender: Any) {

    self.displayAlert("Alert!", message: "USD Selected")

    let usdVal = (outputCurrency1.text! as NSString).floatValue

    let euroValue = usdVal * (usdtoeurquote["5. Exchange Rate"] as! Float)
    outputCurrency2.text = String(format: "%.2f", euroValue)

    let gbpVal = usdVal * (usdtogbpquote["5. Exchange Rate"] as! Float)
    outputCurrency3.text = String(format: "%.2f", gbpVal)

    let cnyVal = usdVal * (usdtocnyquote["5. Exchange Rate"] as! Float)
    outputCurrency2.text = String(format: "%.2f", cnyVal)

    let cadVal = usdVal * (usdtocadquote["5. Exchange Rate"] as! Float)
    outputCurrency2.text = String(format: "%.2f", cadVal)

    let inrVal = usdVal * (usdtoinrquote["5. Exchange Rate"] as! Float)
    outputCurrency2.text = String(format: "%.2f", inrVal)

    let sekVal = usdVal * (usdtosekquote["5. Exchange Rate"] as! Float)
    outputCurrency2.text = String(format: "%.2f", sekVal)

    let rubVal = usdVal * (usdtorubquote["5. Exchange Rate"] as! Float)
    outputCurrency2.text = String(format: "%.2f", rubVal)

    let nzdVal = usdVal * (usdtonzdquote["5. Exchange Rate"] as! Float)
    outputCurrency2.text = String(format: "%.2f", nzdVal)

}

The Raw JSON Data:The json data on the webpage

jww
  • 97,681
  • 90
  • 411
  • 885
devdchaudhary
  • 467
  • 2
  • 13
  • First of all a JSON dictionary in Swift 3+ is `[String:Any]`. Please (learn to) read JSON. It's very simple. For example everything in double quotes is `String` (no exceptions). So the value for key `5. Exchange Rate` is `String`, not `Float`. Further I recommend to use the `Decodable` protocol to parse the JSON into structs. – vadian Jul 22 '18 at 16:59
  • So I can't do any calculations with the value I can only download it? – devdchaudhary Jul 22 '18 at 17:14
  • You can, but you have to create a `Double` or `Float` from the `String` (but please not via `NSString` > `floatValue`) – vadian Jul 22 '18 at 17:15
  • Ok fine thanks, so why does it say everytime I try to download the values that it could not find optional value? Is the jsonserialization correct? – devdchaudhary Jul 22 '18 at 17:27
  • The exception occurs in `usdConversions` because as already mentioned the rates are strings, not floats. Don't use `NSDictionary`, cast to `[String:Any]` and the value for key `Realtime Currency Exchange Rate` is actually more specific `[String:String]`. And omit the `options` parameter. `.allowFragments` is pointless in this case. – vadian Jul 22 '18 at 17:30
  • Now I get a different type of error: Could not cast value of type '__NSCFString' (0x10e857168) to 'NSNumber. Could you please help with this? – devdchaudhary Jul 22 '18 at 17:40
  • Sorry for asking so many questions btw, ur the only one helping out thanks for that. – devdchaudhary Jul 22 '18 at 17:45
  • You have to change a lot of things. I wrote an answer with a more convenient approach. – vadian Jul 22 '18 at 18:04

1 Answers1

1

The main error is that the value for key 5. Exchange Rate is String not Float and the code crashes reliably when being force unwrapped.

In Swift 4 the Decodable protocol is much more convenient than a dictionary.

Create two structs. The ExchangeRate struct contains a computed property floatRate to return the Float value

struct Root : Decodable {
    let exchangeRate : ExchangeRate
    private enum  CodingKeys: String, CodingKey { case exchangeRate = "Realtime Currency Exchange Rate" }
}

struct ExchangeRate : Decodable {

    let fromCode, fromName, toCode, toName, rate : String
    let lastRefreshed, timeZone : String

    private enum  CodingKeys: String, CodingKey {
        case fromCode = "1. From_Currency Code"
        case fromName = "2. From_Currency Name"
        case toCode = "3. To_Currency Code"
        case toName = "4. To_Currency Name"
        case rate = "5. Exchange Rate"
        case lastRefreshed = "6. Last Refreshed"
        case timeZone = "7. Time Zone"
    }

    var floatRate : Float {
        return Float(rate) ?? 0.0
    }
}

Then make the download function more versatile by passing from and to currency as parameters. This version uses the JSONDecoder and returns either the ExchangeRate instance or the Error

func downloadRate(from: String, to: String, completionHandler: @escaping (ExchangeRate?, Error?) -> Void) {

    let urlString = "https://www.alphavantage.co/query?function=CURRENCY_EXCHANGE_RATE&from_currency=\(from)&to_currency=\(to)&apikey=NP3M8LL62YJDO0YX"
    let task = URLSession.shared.dataTask(with: URL(string: urlString)!, completionHandler: { data, response, error in
        if error != nil {
            completionHandler(nil, error!)
        } else {
            do {
                let result = try JSONDecoder().decode(Root.self, from: data!)
                completionHandler(result.exchangeRate, nil)
            } catch {
                completionHandler(nil, error)
            }
        }
    })
    task.resume()
}

and use it

func usdQUotesRequest() {

  USDClient().downloadRate(from:"USD", to: "EUR") { exchangeRate, error in

    if let exchangeRate = exchangeRate {
        self.usdtoeurquote = exchangeRate.floatRate
        DispatchQueue.main.async {
            self.stopActivityIndicator()
            self.Refresh.isEnabled = true
        }
    } else {
        DispatchQueue.main.async {
            self.displayAlert("Unable to Retrieve Latest Conversion Rates", message: "\(error!)")
            self.stopActivityIndicator()
            self.Refresh.isEnabled = true
        }
    }
}
vadian
  • 274,689
  • 30
  • 353
  • 361
  • Oh thanks! On the part of self.usdtoeurquote = exchangeRate.floarRate however I get an error: Cannot assign value of type 'Float?' to type '[String : Any]'. I declared the usdtoeurqutoe as var usdtoeurquote = [String:Any](). What is wrong now? – devdchaudhary Jul 23 '18 at 07:44
  • My code replaced the dictionary with a custom struct. If you are interested in all fields of the decoded JSON declare `usdtoeurquote` as `ExchangeRate` assign the struct instance otherwise as `Float` And assign the rate. – vadian Jul 23 '18 at 08:16
  • Yes it worked! Thanks a lot! I still get a warning though for var usdtoeurquote = ExchangeRate.init(fromCode: "1. From_Currency Code", fromName: "2. From_Currency Name", toCode: "3. To_Currency Code", toName: "4. To_Currency Name", rate: "5. Exchange Rate", lastRefreshed: "6. Last Refreshed", timeZone: "7. Time Zone") as? Float, which is "Cast from 'ExchangeRate' to unrelated type 'Float' always fails". – devdchaudhary Jul 23 '18 at 13:18
  • Don't cast `ExchangeRate` to `Float`, they are two completely different types. Please use exactly my code (`self.usdtoeurquote = exchangeRate.floatRate`). The `ExchangeRate` struct has a computed property `floatRate` which returns a `Float` created from the decoded `rate` String. – vadian Jul 23 '18 at 13:36