1
votes

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! :)

1
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
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 meWizzardzz
@Wizzardzz note that you are displaying euro price using the dollar currency symbol.Leo Dabus

1 Answers

2
votes

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