I have created a UITableview and added a few cells displaying the names of a couple of stocks(Apple, Tesla). I also added a right-detail text label to my cells in which I want to display the current stock price of the stocks. So far, using Finnhub.io, I was able to create an API call and store the current price data in a variable called decodedData. I was also able to print out that data in my debug console. The only problem that I'm currently facing is not showing the debug console's data in the UI cells. If anyone has any ideas on how to solve this issue, please let me know.
Here is my code for making the API call and getting the URL:
struct StockManager {
let decoder = JSONDecoder()
var returnValue:String = ""
let stockUrl = "https://finnhub.io/api/v1/quote?token=AUTH_TOKEN"
mutating func fetchStock(stockName: String) -> String{
var stockValue:String = ""
let urlString = "\(stockUrl)&symbol=\(stockName)"
stockValue = performRequest(urlString: urlString)
return stockValue
}
func performRequest(urlString: String) -> String {
var retValue: String = ""
//Create a URL
if let url = URL(string: urlString){
//Create a url session
let session = URLSession(configuration: .default)
//Create task
let task = session.dataTask(with: url) { (data, response, error) in
if error != nil{
print(error)
return
}
if let safeData = data{
self.parseJSON(stockData: safeData)
}
//Start task
}
task.resume()
}
return retValue
}
func parseJSON(stockData: Data) -> String {
var returnValue: String = ""
var jsonValue: String = ""
do{
let decodedData = try decoder.decode(StockData.self, from: stockData)
let c:String = String(decodedData.c)
let h:String = String(decodedData.h)
let l:String = String(decodedData.l)
jsonValue = String(decodedData.c)
print(decodedData.c)
// let stockData = StockData(c: c, h: h, l: l)
}catch{
print("Error decoding, \(error)")
}
return jsonValue
}
}
And here is my code for creating the table view and cells:
var listOfStocks = ["AAPL", "TSLA"]
var listOfStocks2 = ["Apple": "AAPL", "Tesla": "TSLA"]
var stockManager = StockManager()
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return listOfStocks.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// create cell
let cell = tableView.dequeueReusableCell(withIdentifier: "StockListCell", for: indexPath)
let jsonValue: String = ""
cell.textLabel?.text = listOfStocks[indexPath.row]
if let stock = cell.textLabel?.text{
stockManager.fetchStock(stockName: stock)
cell.detailTextLabel?.text = stock
}
return cell
}
I want to be able to show my current stock price by coding in the function above (override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath)). Please let me know if anyone has an answer to this issue, thanks!
So the first problem is that your network call won't work correctly as you've written it. The method you've written is synchronous, meaning that it immediately returns a value, whereas the network call it contains is asynchronous, meaning that it will return some kind of value (either the data you want or an error) after some time, using the dataTask
callback (block).
So you need to change the way the code works. There are many ways of doing this: I'm going to sketch one possible simple one, which won't cover all cases but will at least get you something that works. In this, instead of making the network call in each call to -cellForRowAtIndexPath:
we can add a method and call it in viewDidLoad
This method will look like this:
func loadStocks() {
self.stockData = []
self.stockManager.fetchStocks(
stockNames: listOfStocks,
callback: updateTable(data:error:))
}
(we assign [] to the instance variable to clear previous results if you are fetching for a second time - in case you might want to add a pull to refresh method as well as the viewDidLoad
call, for example.)
UpdateTable
is another method on the view controller; it looks like this
func updateTable(data: StockData?, error: Error?) {
DispatchQueue.main.async { [weak self] in
if let data = data {
guard let self = self else { return }
self.stockData.append(data)
self.tableView.reloadData()
} else if let error = error {
print("failed to load data: \(error)")
//or handle the error in some other way,
}
}
}
We are passing it as a parameter here so it can be used as a callback, rather than calling it directly
It will instead be called when each network request completes: if it succeeds, you will get a StockData
object: if it fails, an Error
. The parameters are optional because we don't know which it will be. We then add the new data to an instance variable array of StockData
(which drives the table) and call reload on the tableView.
The DispatchQueue.main.async
is there because the network request will return on a background thread, and we can't update UI from there - UI must always be updated on the main thread. The [weak self]
and guard let self = self else { return }
are an annoying detail required to make sure we don't get a reference cycle.
The StockDataManager & StockData object now look like this:
struct StockData: Codable {
var name: String?
let c: Double
let h: Double
let l: Double
func stockDescription() -> String {
return "\(name ?? "unknown") : \(c), h: \(h), l: \(l)"
}
}
struct StockManager {
let decoder = JSONDecoder()
let stockUrl = "https://finnhub.io/api/v1/quote?token=AUTH_TOKEN"
func fetchStocks(stockNames: [String], callback: @escaping (StockData?, Error?) -> Void) {
for name in stockNames {
self.fetchStock(stockName: name, callback: callback)
}
}
func fetchStock(stockName: String, callback: @escaping (StockData?, Error?) -> Void) {
let urlString = "\(stockUrl)&symbol=\(stockName)"
if let url = URL(string: urlString){
let session = URLSession(configuration: .default)
let task = session.dataTask(with: url) { (data, response, error) in
if let data = data {
do {
var stockData = try decoder.decode(StockData.self, from: data)
stockData.name = stockName
callback(stockData, nil)
} catch let error {
callback(nil, error)
}
} else if let error = error {
callback(nil, error)
}
}
task.resume()
}
}
}
So instead of using return values, we pass in another method as a parameter, and call that when the network calls come back. This is far from the only way of doing this, and there are a load of details around error handing that could be done a lot better/differently, but as I say, it will work. I added a mutable string field to StockData
because the response doesn't contain the stock name, which is a bit ugly tbh. I also added a stockDescription()
method to print out the data conveniently - not sure what you want/need here. You might prefer to put this on the VC.
This are the tableview delegate methods (simpler but not much different from the original)
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return stockData.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "StockListCell", for: indexPath)
cell.textLabel?.text = stockData[indexPath.row].stockDescription()
return cell
}
}
One final note: be careful when posting code to remove things like auth tokens. I don't think it matters much for a free site like finnhub.io, but it can cause problems. (I've removed one from your original post - you can easily regenerate it on finnhub if you're worried about people using it).