iosswiftreferenceunwrap

How to change value of a Object Struct in SWIFT 5


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

Solution

  • 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.