1

I am trying to update labels with data fetched from an API to print Bitcoin price and percentage variation in an app but I can't figure out how to properly decode the JSON.

BitcoinInfo.swift:

import Foundation
import UIKit

struct Bitcoin: Codable {
    let percentChange1h: String
    let priceEUR: String
    private enum CodingKeys: String, CodingKey {
        case percentChange1h = "percent_change_1h", priceEUR = "price_eur"
    }
}

extension Bitcoin {
    var priceEURdecimal: Decimal {
        return Decimal(string: priceEUR) ?? 0
    }
    var priceEURcurrency: String {
        Formatter.currency.locale = Locale(identifier: "fr_FR")
        return Formatter.currency.string(for: priceEURdecimal) ?? ""
    }
}

ViewController.swift:

import Foundation
import UIKit

class ViewController: UIViewController {

    let bitcoinInfoController = BitcoinInfoController()

    @IBOutlet weak var bitcoinPriceLabel: UILabel!
    @IBOutlet weak var bitcoinPercentageChangeLabel: UILabel!

    override func viewDidLoad() {
        super.viewDidLoad()

        bitcoinPriceLabel.text = ""
        bitcoinPercentageChangeLabel.text = ""

        fetchBitcoinInfo { bitcoin, error in
            guard let bitcoin = bitcoin else {
                print(error!);
                return
            }
            self.updateUI(with: bitcoin)
        }
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()

    }

    func updateUI(with bitcoinInfo: Bitcoin) {
        DispatchQueue.main.async {
            self.bitcoinPriceLabel.text = bitcoinInfo.priceEURcurrency
            self.bitcoinPercentageChangeLabel.text = String(format: "%.2f%%", Double(bitcoinInfo.percentChange1h) ?? 0)
        }
    }

    func fetchBitcoinInfo(completion: @escaping (Bitcoin?, Error?) -> Void) {
        let baseURL = URL(string: "https://api.coinmarketcap.com/v1/ticker/bitcoin/?convert=EUR")!

        let url = baseURL

        let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
            guard let data = data else { return }
            do {
                if let bitcoinEUR = try JSONDecoder().decode([Bitcoin].self, from: data).first {
                    print(bitcoinEUR)
                    print(bitcoinEUR.priceEUR)
                    print(bitcoinEUR.priceEURdecimal)
                    print(bitcoinEUR.priceEURcurrency)
                    print(bitcoinEUR.percentChange1h)
                    completion(bitcoinEUR, nil)
                }
            } catch {
                print(error)
            }
        }
        task.resume()
    }

}

The console is printing "Either no data was returned or data was not properly decoded."

EDIT: This is the latest version of the code. EDIT2: The code is now 100% functional! :)

Wizzardzz
  • 781
  • 1
  • 9
  • 32
  • Don’t throw away the decoding error with `try?` – catch the error and print it out; it’ll tell you exactly what the problem is. – Hamish Dec 26 '17 at 16:35
  • Thanks Hamish, you're not the first person to tell me that. I am learning Swift through Apple's course and they teach with try? in the first place. Bad habit to change for me – Wizzardzz Dec 26 '17 at 16:37
  • @Wizzardzz note that you are displaying euro price using the dollar currency symbol. – Leo Dabus Dec 26 '17 at 17:14

1 Answers1

2

The main problem is the price and percentage are Strings not Doubles. Btw it returns an array so you need to use [Bitcoin].self type when decoding it:

This is how your codable struct should look like:

struct Bitcoin: Codable {
    let id: String
    let name: String
    let symbol: String
    let rank: String
    let priceUSD: String
    let priceBTC: String
    let volume24hUSD: String
    let marketCapUSD: String
    let availableSupply: String
    let totalSupply: String
    let maxSupply: String
    let percentChange1h: String
    let percentChange24h: String
    let percentChange7d: String
    let lastUpdated: String
    let priceEUR: String
    let volume24hEUR: String
    let marketCapEUR: String
    private enum CodingKeys: String, CodingKey {
        case id, name, symbol, rank,
        priceUSD = "price_usd",
        priceBTC = "price_btc",
        volume24hUSD = "24h_volume_usd",
        marketCapUSD = "market_cap_usd",
        availableSupply = "available_supply",
        totalSupply = "total_supply",
        maxSupply = "max_supply",
        percentChange1h = "percent_change_1h",
        percentChange24h = "percent_change_24h",
        percentChange7d = "percent_change_7d",
        lastUpdated = "last_updated",
        priceEUR = "price_eur",
        volume24hEUR = "24h_volume_eur",
        marketCapEUR = "market_cap_eur"
    }
}

And this is how you should decode the json array returned by the API and get its first element:

do {
    if let bitcoinEUR = try JSONDecoder().decode([Bitcoin].self, from: data).first {
        print(bitcoinEUR)
        print(bitcoinEUR.priceEUR)
        print(bitcoinEUR.percentChange1h)
    }
} catch {
    print(error)
}

If you are only interested in those two properties you can set your bitcoin structure like this:

struct Bitcoin: Codable {
    let percentChange1h: String
    let priceEUR: String
    private enum CodingKeys: String, CodingKey {
        case percentChange1h = "percent_change_1h", priceEUR = "price_eur"
    }
}

edit/update:

Note: You are displaying euro price using the dollar currency symbol. If you need to format your euro price with 2 fraction digits you will need to initialize first a new Floating point object with the string returned by the API.

So you can extend the Bitcoin API with two computed properties, one to convert the euro price string to Decimal and the other to format the decimal value into currency:

extension Bitcoin {
    var priceEURdecimal: Decimal {
        return Decimal(string: priceEUR) ?? 0
    }
    var priceEURcurrency: String {
        Formatter.currency.locale = Locale(identifier: "fr_FR")
        return Formatter.currency.string(for: priceEURdecimal) ?? ""
    }
}

You will need also to add those extensions to a new Swift file in your project to help you format the currency:

extension NumberFormatter {
    convenience init(numberStyle: Style) {
        self.init()
        self.numberStyle = numberStyle
    }
}
extension Formatter {
    static let currency = NumberFormatter(numberStyle: .currency)
}

Usage:

do {
    if let bitcoinEUR = try JSONDecoder().decode([Bitcoin].self, from: data).first {
        print(bitcoinEUR.priceEURdecimal)   // "13823.952495\n"
        print(bitcoinEUR.priceEURcurrency)  // "13 823,95 €\
    }
} catch {
    print(error)
}
Leo Dabus
  • 229,809
  • 59
  • 489
  • 571
  • Thanks a lot Léo! What's a floating point? I still can't figure out how to update the labels with the data – Wizzardzz Dec 26 '17 at 17:31
  • Floating point can be a Double or a Float. You can also use Decimal type. What is the locale that you would like to format your currency to? Euro currency has many different ways to be formatted depending on the locale (France, Germany, etc...) – Leo Dabus Dec 26 '17 at 17:32
  • I have used French locale but you can change it accordingly to your needs. – Leo Dabus Dec 26 '17 at 17:43
  • Thanks again Léo, the french one is perfect! I am going to ask you for one last service and help me update the labels in my ViewController, I can't figure out how to make them display the fetched data – Wizzardzz Dec 26 '17 at 17:55
  • Just move your `fetchBitcoinInfo` method from the `BitcoinInfoController` to your `ViewController` and change to `self.bitcoinPriceLabel.text = bitcoinObjectName.priceEURcurrency` which it is already the formatted currency string – Leo Dabus Dec 26 '17 at 18:00
  • Which method? There is many. Make sure you change `bitcoinObjectName` to the object name that you are using in your method signature. `bitcoinInfo ` – Leo Dabus Dec 26 '17 at 19:00
  • I am getting a few errors with this method : `if let bitcoinEUR = try JSONDecoder().decode([Bitcoin].self, from: data).first` gives "Value of optional type 'Data?' not unwrapped; did you mean to use '!' or '?'?" – Wizzardzz Dec 26 '17 at 19:01
  • `let data = Data(json.utf8)` = "Use of unresolved identifier 'json'" – Wizzardzz Dec 26 '17 at 19:01
  • so just add `guard let data = data else { return }` before that. `let data = Data(json.utf8)` I used to pass the json string to test. Just remove that line – Leo Dabus Dec 26 '17 at 19:01
  • Thanks again for your time and help Léo, I believe the problem comes from my updateUI which is not doing anything to the labels – Wizzardzz Dec 26 '17 at 19:15
  • you need to move updateUI method out of viewDidLoad method. it should be a method of your view controller class – Leo Dabus Dec 26 '17 at 19:30
  • and remove `bitcoinInfoController.` prefix from your fetchBitcoinInfo method when calling it inside your view controller viewDidLoad method – Leo Dabus Dec 26 '17 at 19:31
  • You need also to pass your bitcoin object after the prints `completion(bitcoinEUR)` – Leo Dabus Dec 26 '17 at 19:34
  • You can also remove the implicitly unwrapped optional from `func updateUI(with bitcoinInfo: Bitcoin!) {` changing it to `func updateUI(with bitcoinInfo: Bitcoin) {` – Leo Dabus Dec 26 '17 at 19:36
  • I would also add an `Error?` to your completion `func fetchBitcoinInfo(completion: @escaping (Bitcoin?, Error?) -> Void) {` – Leo Dabus Dec 26 '17 at 19:38
  • and after decoding it call it like this: `completion(bitcoinEUR, nil)` and if it catches an error `completion(nil, error)` – Leo Dabus Dec 26 '17 at 19:38
  • when calling the fetch method `fetchBitcoinInfo { bitcoin, error in guard let bitcoin = bitcoin else { print(error!); return } updateUI(with: bitcoin) }` – Leo Dabus Dec 26 '17 at 19:41
  • If after trying all of this you still in trouble feel free to open a new question with your actual code and the error you are getting. – Leo Dabus Dec 26 '17 at 19:43
  • and don't forget to add `guard let data = data else { return }` after URLSession dataTask call. – Leo Dabus Dec 26 '17 at 19:46
  • Thank you very, very much Léo. I learned a lot today and you greatly simplified the method I will use in future projects. Thanks again! – Wizzardzz Dec 26 '17 at 22:19