Reputation: 841
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! :)
Upvotes: 1
Views: 1452
Reputation: 236420
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)
}
Upvotes: 2