Reputation: 1757
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
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