JS_is_awesome18
JS_is_awesome18

Reputation: 1757

How to add data from API call to tableviewcell in Swift

I am setting up a basic app that returns values from an API call to cells in a tableview. Various examples I've seen online configure the service file to decode directly from a model. However, I am trying to set up my service file so that it decodes from the ViewModel, if possible. When I run the app in Xcode I do not get errors, but I'm also seeing blank cells in my tableview. I am still fairly new to API calls in Swift. Any idea how I can configure this to return API call values to the cells while still using a viewModel? See my code below:

In my WebService file, I set up my URLSession as follows:

class WebService {
        
    func getStocks(completion: @escaping (([SymbolViewModel]?) -> Void)) {

        guard let url = URL(string: "https://island-bramble.glitch.me/stocks") else {
            fatalError("URL is not correct")
        }

        URLSession.shared.dataTask(with: url) { data, response, error in

            guard let data = data, error == nil else {
                completion(nil)
                return
            }

            let symbols = try? JSONDecoder().decode([SymbolViewModel].self, from: data)
            symbols == nil ? completion(nil) : completion(symbols)

        }.resume()

    }
}

The ViewModel is set up like so:

struct SymbolViewModel:Decodable {
 
    let stockSymbol: Symbol
    
    var symbol: String {
        return self.stockSymbol.symbol.uppercased()
    }
    
    var price: String {
        return String(format: "%.2f", self.stockSymbol.price)
    }
}

The Model:

struct Symbol: Decodable {
    let symbol: String
    let price: Double
}

The ViewController with the TableView functions and TableViewCell:

class ViewController: UIViewController {
    
    let webService = WebService()
    var symbols = [SymbolViewModel]()
    
    @IBOutlet var tableView: UITableView!
        
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let nib = UINib(nibName: "TableViewCell", bundle: nil)
        tableView.register(nib, forCellReuseIdentifier: "TableViewCell")
        tableView.delegate = self
        tableView.dataSource = self
                
        webService.getStocks { symbols in
            if let symbols = symbols {
                DispatchQueue.main.async {
                    self.symbols = symbols
                }
            }
        }
        self.tableView.reloadData()
    }
}

class TableViewCell: UITableViewCell {
    
    @IBOutlet var mySymbol: UILabel!
    @IBOutlet var myPrice: UILabel!

}

extension ViewController: UITableViewDelegate, UITableViewDataSource {
    /// tableView functions
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return symbols.count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        
        let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: TableViewCell.self), for: indexPath) as! TableViewCell
        
        cell.mySymbol.text = symbols[indexPath.row].symbol
        cell.myPrice.text = symbols[indexPath.row].price

        return cell
    }
}

ERROR MESSAGE:

keyNotFound(CodingKeys(stringValue: "stockSymbol", intValue: nil), Swift.DecodingError.Context(codingPath: [_JSONKey(stringValue: "Index 0", intValue: 0)], debugDescription: "No value associated with key CodingKeys(stringValue: \"stockSymbol\", intValue: nil) (\"stockSymbol\").", underlyingError: nil))
0 elements

Upvotes: 0

Views: 1827

Answers (1)

vadian
vadian

Reputation: 285059

Your model is wrong.

First of all if you declare computed properties you have to add CodingKeys to prevent the computed properties from being decoded. This is the main reason of the error, however there are more.

The JSON is an array of (one-dimensional) dictionaries, the corresponding model is

struct StockSymbol : Decodable {
    
    let symbol : String
    let price : Double
    
    private enum CodingKeys : String, CodingKey { case symbol, price }
    
    var priceAsString: String {
        return String(format: "%.2f", price)
    }
}

The other struct Symbol is not needed.

Decode it

do {
    let symbols = try? JSONDecoder().decode([StockSymbol].self, from: data)
    completion(symbols)
} catch {
    print(error)
    completion(nil)
}
 

and you have to reload the table view inside the completion handler

webService.getStocks { symbols in
    if let symbols = symbols {
        DispatchQueue.main.async {
            self.symbols = symbols
            self.tableView.reloadData()
        }
    }
}
   

A more sophicticated way is the Result type, it returns either the valid data or the error

class WebService {
    
    func getStocks(completion: @escaping (Result<[StockSymbol],Error>) -> Void) {
        
        guard let url = URL(string: "https://island-bramble.glitch.me/stocks") else {
            fatalError("URL is not correct")
        }
        
        URLSession.shared.dataTask(with: url) { data, response, error in
            if let error = error { completion(.failure(error)); return }
            completion( Result {try JSONDecoder().decode([StockSymbol].self, from: data!)})
        }.resume()
        
    }
}

and call it

webService.getStocks { [weak self]  result in
    switch result {
        case .success(let symbols):
            DispatchQueue.main.async {
                self?.symbols = symbols
                self?.tableView.reloadData()
            }
        case .failure(let error): print(error) // or show the error to the user
    }
}

Upvotes: 1

Related Questions