1

I am trying to get value from JSON type, bring it to my var cryptos. But nothing change, var cryptos still nil. Even value of cryptos in Update void has changed. I have struggled with this problem for many hours. Thanks for your answer. This is my code:

var cryptos: Crypto? = nil
override func viewDidLoad() {
    super.viewDidLoad()
    update()
    //I can't print this. ERROR: Unexpectedly found nil while unwrapping an Optional value
    //print(self.cryptos!.data[0].name)
    tableView.delegate = self
    tableView.dataSource = self
 // Do any additional setup after loading the view.
}

@objc func update() {
    if let url = URL(string:"https://pro-api.coinmarketcap.com/v1/cryptocurrency/listings/latest") {
                var request = URLRequest(url: url)
                request.addValue("305782b4-...-1835adfe147a", forHTTPHeaderField: "X-CMC_PRO_API_KEY")
                request.httpMethod = "GET"
                let dataTask = URLSession.shared.dataTask(with: request) { (data: Data?, response: URLResponse?, error: Error?) in
                    do {
                        do {
                            let response = try JSONDecoder().decode(Crypto.self, from: data!)     
                            self.cryptos = response
                            //but here I can. Why??
                            print(self.cryptos!.data[0].name)
                        } catch { print(error) }
                        }
                    }
                dataTask.resume()
        }
}

This is my Struct:

public struct Crypto : Codable {
    struct data : Codable {
        let id: Int
        let name: String
        let symbol: String
        let cmc_rank: Int
        let slug: String
        struct quote : Codable {
            struct USD : Codable {
                let price: Double
                let volume_24h: Double
                let percent_change_1h: Double
                let percent_change_24h: Double
                let percent_change_7d: Double
            }
            let USD: USD
        }
        let quote: quote
    }
    let data: [data]
}
Rob
  • 415,655
  • 72
  • 787
  • 1,044
Tuan Ho Si
  • 25
  • 5

2 Answers2

0

Add a completion block to the function because it's an asynchronous task.

override func viewDidLoad() {
    super.viewDidLoad()
    update {
        print(self.cryptos?.data[0].name ?? "")
    }
    //...
}
@objc func update(completion: @escaping () -> Void) {
    if let url = URL(string:"https://pro-api.coinmarketcap.com/v1/cryptocurrency/listings/latest") {
        //...
        let dataTask = URLSession.shared.dataTask(with: request) { (data: Data?, response: URLResponse?, error: Error?) in
            do {
                let response = try JSONDecoder().decode(Crypto.self, from: data!)
                self.cryptos = response
                print(self.cryptos!.data[0].name)
                completion()
            } catch {
                print(error)
                completion()
            }
        }
        dataTask.resume()
    }
}
Frankenstein
  • 15,732
  • 4
  • 22
  • 47
0

The update runs asynchronously. So supply it a completion handler:


A few suggestions:

  1. I'd define a response object to wrap the payload. The data key is not something you need to perpetuate in your model objects:

     public struct ResponseObject<T: Codable>: Codable {
         let data: [T]
     }
    
     struct Crypto: Codable {
         let id: Int
         let name: String
         let symbol: String
         let cmc_rank: Int
         let slug: String
         struct Quote : Codable {
             struct USD : Codable {
                 let price: Double
                 let volume_24h: Double
                 let percent_change_1h: Double
                 let percent_change_24h: Double
                 let percent_change_7d: Double
             }
             let USD: USD
         }
         let quote: Quote
     }
    

    I'd be inclined to use the above generic pattern, so you can reuse this ResponseObject pattern elsewhere.

  2. I'd give update a completion handler that uses the Result type:

     @discardableResult
     func update(completion: @escaping (Result<[Crypto], Error>) -> Void) -> URLSessionTask {
         let url = URL(string:"https://pro-api.coinmarketcap.com/v1/cryptocurrency/listings/latest")!
         var request = URLRequest(url: url)
         request.addValue("305782b4-...-1835adfe147a", forHTTPHeaderField: "X-CMC_PRO_API_KEY")
         let task = URLSession.shared.dataTask(with: request) { data, response, error in
             guard let responseData = data, error == nil else {
                 DispatchQueue.main.async { completion(.failure(error ?? NetworkError.unknownError(data, response))) }
                 return
             }
    
             do {
                 let response = try JSONDecoder().decode(ResponseObject<Crypto>.self, from: responseData)
                 DispatchQueue.main.async { completion(.success(response.data)) }
             } catch {
                 DispatchQueue.main.async { completion(.failure(error)) }
             }
         }
         task.resume()
         return task
     }
    

    Where

     enum NetworkError: Error {
         case unknownError(Data?, URLResponse?)
     }
    

    FYI, in addition to a completion handler closure, I've removed the force unwrapping operator (!).

    I'd also make this return the URLSessionTask (in case the caller might, at some future date, want to be able to cancel the request for any reason). By making it @discardableResult, it means that you don't require the caller to do anything with the returned value, but you're leaving that door open for the future.

  3. The viewDidLoad could then use that to reload the table as appropriate:

     var cryptos: [Crypto] = []
    
     override func viewDidLoad() {
         super.viewDidLoad()
    
         update() { result in
             switch result {
             case .failure(let error):
                 print(error)
    
             case .success(let cryptos):
                 self.cryptos = cryptos
                 self.tableView.reloadData()
             }
         }
     }
    

    Note, both the update of the model object and the reloading of the table are done in this completion handler, which has been dispatched to the main queue. All UI and model updates should take place on main thread.

Rob
  • 415,655
  • 72
  • 787
  • 1,044
  • I really appreciate. Tried on your suggestion. But, how do I get this value? Example: `cell.nameCrypto.text = cryptos.data[indexPath.row].name cell.nameCrypto.text = cryptos[indexPath.row].data.name` – Tuan Ho Si Jun 29 '20 at 03:02
  • I tried this, and it work as I wish. `let cell = tableView.dequeueReusableCell(withIdentifier: "cellID") as! CryptoTableViewCell update() { result in switch result { case .failure(let error): print(error) case .success(let cryptos): self.cryptos = cryptos cell.nameCrypto.text = cryptos.data[indexPath.row].name } } return cell` – Tuan Ho Si Jun 29 '20 at 03:05
  • Change to `var cryptos: Crypto?` and to `let response = try JSONDecoder().decode(Crypto.self, from: responseData) DispatchQueue.main.async { completion(.success(response))` . Thank you so much. I will upvote when I have enough reputation – Tuan Ho Si Jun 29 '20 at 03:07
  • With my revision to your model, it would be `cell.label.text = cryptos[indexPath.row].name`. This `data` key is present in your JSON, but doesn’t belong in your model. It’s only there for the JSON parsing (which is why I’ve relegated it to the `ResponseObject` generic). So, no, my revised model and revised `update` method should stay as I’ve written them. – Rob Jun 29 '20 at 03:38