Wizzardzz
Wizzardzz

Reputation: 841

Decoding API data

I'm trying to fetch data from an API but I can't get it right and I don't know the issue here:

struct BTCData : Codable {
    let close : Double
    let high : Double
    let low : Double

    private enum CodingKeys : Int, CodingKey {
        case close = 3
        case high = 4
        case low = 5
    }
}

func fetchBitcoinData(completion: @escaping (BTCData?, Error?) -> Void) {
    let url = URL(string: "https://api.bitfinex.com/v2/candles/trade:30m:tBTCUSD/hist")!

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

I'd like to be able to access close in every dict and iterate like that:

var items : BTCData!

for idx in 0..<15 {
    let diff = items[idx + 1].close - items[idx].close
    upwardMovements.append(max(diff, 0))
    downwardMovements.append(max(-diff, 0))
}

I get nil. I don't understand how to decode this kind of API where I need to iterate something which is not inside another dict.

EDIT: The above was solved and I'm now struggling to use [BTCData] in another function.

I am trying to use it here :

func fetchBitcoinData(completion: @escaping ([BTCData]?, Error?) -> Void) {

    let url = URL(string: "https://api.bitfinex.com/v2/candles/trade:30m:tBTCUSD/hist")!

    let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
        guard let data = data else {
            completion(nil, error ?? FetchError.unknownNetworkError)
            return
        }
        do {
            let bitcoin = try JSONDecoder().decode([BTCData].self, from: data); completion(bitcoin, nil)
            //let close52 = bitcoin[51].close
            //print(bitcoin)
            //print(close52)
        } catch let parseError {
            completion(nil, parseError)
        }
    }
    task.resume()
}
class FindArray {

    var items = [BTCData]()

    func findArray() {
        let close2 = items[1].close
        print(close2)
    }
}

fetchBitcoinData() { items, error in
    guard let items = items,
        error == nil else {
            print(error ?? "Unknown error")
            return
    }
    let call = FindArray()
    call.items = items
    call.findArray()
}

EDIT 2: Solved it with [BTCData](). var items : [BTCData] = [] works too

Upvotes: 1

Views: 223

Answers (2)

vadian
vadian

Reputation: 285079

To decode an array of arrays into a struct with Decodable you have to use unkeyedContainer. Since there is no dictionary CodingKeys are useless.

struct BTCData : Decodable {
    let timestamp : Int 
    let open, close, high, low, volume : Double

    init(from decoder: Decoder) throws {
        var container = try decoder.unkeyedContainer()
        timestamp = try container.decode(Int.self)
        open = try container.decode(Double.self)
        close = try container.decode(Double.self)
        high = try container.decode(Double.self)
        low = try container.decode(Double.self)
        volume = try container.decode(Double.self)
    }
}

You don't have to change your JSONDecoder() line.

...
if let bitcoin = try JSONDecoder().decode([BTCData].self, from: data).first {
   print(bitcoin)
   completion(bitcoin, nil)
}

Just by adding two lines it's even possible to decode the timestamp into a Date value

struct BTCData : Decodable {
    let timestamp : Date
    let open, close, high, low, volume : Double

    init(from decoder: Decoder) throws {
        var container = try decoder.unkeyedContainer()
        timestamp = try container.decode(Date.self)
...

let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .millisecondsSince1970
if let bitcoin = try decoder.decode([BTCData].self, from: data).first {
    print(bitcoin)
    completion(bitcoin, nil)
}

To decode the array and get a value at specific index

do {
    let bitcoins = try JSONDecoder().decode([BTCData].self, from: data)
    let close52 = bitcoins[51].close
    print(close52)
...

Upvotes: 2

Reinier Melian
Reinier Melian

Reputation: 20804

You need to use JSONSerialization and cast to [[NSNumber]] to get the result needed

UPDATE

Checking this https://docs.bitfinex.com/v2/reference#rest-public-candles I think this is what you are searching for

Try using this

func fetchBitcoinData(completion: @escaping ([BTCData]?, Error?) -> Void) {
    let url = URL(string: "https://api.bitfinex.com/v2/candles/trade:30m:tBTCUSD/hist")!

    let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
        guard let data = data else { return }
        do {
                if let array = try JSONSerialization.jsonObject(with: data, options: .allowFragments) as? [[NSNumber]]{
                                            var arrayOfCoinData : [BTCData] = []
                    for currentArray in array{
                        arrayOfCoinData.append(BTCData(close: currentArray[2].doubleValue, high: currentArray[3].doubleValue, low: currentArray[4].doubleValue))
                    }
                    debugPrint(arrayOfCoinData)
                    completion(arrayOfCoinData, nil)
                }
        } catch {
            print(error)
            completion(nil, error)
        }
    }
    task.resume()
}

Log Result

[BitcoinApiExample.BTCData(close: 7838.8999999999996,...]

Upvotes: 1

Related Questions