TableView loads cell before API request can update model - ios

I have a tableView controller where each line will represent the value of a specific cryptocurrency. I have a class for each different cryptocurrency the user wants to track called CryptoCurrency. Under the TableView CellforRowAt function I'm calling another function called getCryptoData, that will use AlamoFire to send api request to get the price of each cryptocurrency.
The problem is that the cellForRowAt function returns a cell with the default price of 0.00 before the getCryptoData function can finish and update the model.
I'm assuming this is because that function is run asynchronously?
How do I make it either wait for the function to finish before returning the cell or reload the cell after it is finished?
I tried adding tableView.reloaddata() at the end of the updateCryptoData function but that resulted in an endless loop.
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "CryptoCurrencyCell", for: indexPath)
let cryptoCurrencyItem = cryptoCurrencyContainer.listOfCryptoCurrencies[indexPath.row]
getCryptoData(url: getApiString(for: cryptoCurrencyItem.id), currencyItem: cryptoCurrencyItem)
cell.textLabel?.text = cryptoCurrencyContainer.listOfCryptoCurrencies[indexPath.row].name + "\(cryptoCurrencyItem.currentPrice)"
print(cryptoCurrencyItem.currentPrice)
return cell
}
func getCryptoData(url: String, currencyItem: CryptoCurrency) {
Alamofire.request(url, method: .get).responseJSON {
response in
if response.result.isSuccess {
print("Sucess! bitcoin data")
let cryptoDataJSON : JSON = JSON(response.result.value!)
print(cryptoDataJSON)
self.updateCryptoData(json: cryptoDataJSON, currencyItem: currencyItem)
} else {
print("oops")
print("Error: \(String(describing: response.result.error))")
//self.bitcoinPriceLabel.text = "Connection Issues"
}
}
}
func updateCryptoData(json : JSON, currencyItem: CryptoCurrency) {
if let cryptoPriceResult = json["ask"].double {
//bitcoinPriceLabel.text = String(bitcoinResult)
currencyItem.updatePrice(price: cryptoPriceResult)
print(currencyItem.currentPrice)
} else {
//bitcoinPriceLabel.text = "Price Unavailable"
print("something aint right")
}
}
Under the cellForRowAt function there is a print statement:
print(cryptoCurrencyItem.currentPrice)
capturing the curent price before the cell is return. The console shows that it is still 0.00 implying that the getCryptoData function hasn't finished running.

Probably it is not the best solution but you could pass the cell to your method
func getCryptoData(url: String, currencyItem: CryptoCurrency, for cell: UITableViewCell) {
Alamofire.request(url, method: .get).responseJSON {
response in
if response.result.isSuccess {
print("Sucess! bitcoin data")
let cryptoDataJSON : JSON = JSON(response.result.value!)
print(cryptoDataJSON)
self.updateCryptoData(json: cryptoDataJSON, currencyItem: currencyItem, for: cell)
} else {
print("oops")
print("Error: \(String(describing: response.result.error))")
//self.bitcoinPriceLabel.text = "Connection Issues"
}
}
}
func updateCryptoData(json : JSON, currencyItem: CryptoCurrency, for cell: UITableViewCell) {
if let cryptoPriceResult = json["ask"].double {
//bitcoinPriceLabel.text = String(bitcoinResult)
currencyItem.updatePrice(price: cryptoPriceResult)
print(currencyItem.currentPrice)
cell.textLabel?.text = "\(cryptoPriceResult)" // your cell
} else {
//bitcoinPriceLabel.text = "Price Unavailable"
cell.textLabel?.text = "\(cryptoPriceResult)" // your cell
print("something aint right")
}
}
Since UITableViewCell is a reference type, once the async call is finished, it will update the cell text label.

Your request is running in a background thread. It means your tableView runs before you get data from request. So when you get success in updateCryptoData method you should call tableView.reloadData() in main thread.
DispatchQueue.main.async {
tableView.reloadData()
}
This code will run tableView cellForRow method

Yes tableView.reloaddata() at the end of the updateCryptoData function will rsult in infinite loop because in every reload after api call you again calling api and cycle continues.
From one of many approach, You can have a data structure of key-value based.
In your cellForRowAt function, you will be assigning value from data sctructure if available to your Label. if not value is available in your data structure then show some default value like "loading" or tiny loading wheel UI.
cell.priceLabel?.text = dataStructre["key"]
Now this key could be your some unique id that you identify your currency items.
Before calling your api from cell, check beforehand if value for "key" is available in your data structure. If value is unavailable then only you will call the api else simple fetch from data structure and set it to your Label.
if let value = dataStructre["key"]{
cell.priceLabel?.text = value
}
else{
callApi()
}
Finally In success method of your api call simple store the value that you got from server in you data structure with the "key identifier". After saving to your data structure, simply call "reloadData".
function callApi(){
if(success){
dataStructure["key"] = value from server
table.reloadData()
}
}

Related

Swift - Firebase - Downloading images and data asynchronously leads to wrong display within the collection view cell

I have a collection view and want to load images and other data asynchronously from firebase and display them within the cell. However, my current approach displays wrong images to the text data (they simply don't fit) and also, the image in the one specific cell changes few times until it settles down (sometime wrong, sometimes correct).
My code
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let photoCell = collectionView.dequeueReusableCell(withReuseIdentifier: "mainViewCollectionCell", for: indexPath) as! MainViewCollectionViewCell
// issue when refreshing collection view after a new challenge has been created
if (hitsSource?.hit(atIndex: indexPath.row) == nil) {
return photoCell
}
let challengeObject = Challenge(json: (hitsSource?.hit(atIndex: indexPath.row))!)
let group = DispatchGroup()
group.enter()
// async call
self.checkIfChallengeIsBlocked(completionHandler: { (IsUserBlocked) in
if (IsUserBlocked) {
return
}
else {
group.leave()
}
}, challengeObject: challengeObject)
group.notify(queue: .main) {
photoCell.setChallengeLabel(title: challengeObject.title)
// async call
photoCell.fetchChallengeImageById(challengeObject: challengeObject)
photoCell.checkIfToAddOrRemovePlayIcon(challengeObject: challengeObject)
// async call
self.dataAccessService.fetchUserById(completionHandler: { (userObject) in
photoCell.setFullName(userObject: userObject)
photoCell.setStarNumber(challengeObject: challengeObject)
}, uid: challengeObject.organizerId)
// async all
self.dataAccessService.fetchAllParticipantsByChallengeId(completionHandler: { (participationArray) in
photoCell.setParticipationNumber(challengeObject: challengeObject, participationArray: participationArray)
}, challengeId: challengeObject.id)
// resize image to collection view cell
self.activityView.removeFromSuperview()
}
return photoCell
}
...
Just to show you my MainViewCollectionViewCell
class MainViewCollectionViewCell: UICollectionViewCell {
...
public func fetchChallengeImageById(challengeObject:Challenge) {
self.dataAccessService.fetchChallengeImageById(completion: { (challengeImage) in
self.challengeImageView.image = challengeImage
self.layoutSubviews()
}, challengeId: challengeObject.id)
}
and DataAccessService.swift
class DataAccessService {
...
// fetch main challenge image by using challenge id
public func fetchChallengeImageById(completion:#escaping(UIImage)->(), challengeId:String) {
//throws {
BASE_STORAGE_URL.child(challengeId).child(IMAGE_NAME).getData(maxSize: 1 * 2048 * 2048,
completion:({ data, error in
if error != nil {
print(error?.localizedDescription as Any)
let notFoundImage = UIImage()
completion(notFoundImage)
} else {
let image = UIImage(data: data!)!
completion(image)
}
}))
}
...
public func fetchUserById(completionHandler:#escaping(_ user: User)->(), uid:String?) { //
throws{
var userObject = User()
let _userId = UserUtil.validateUserId(userId: uid)
USER_COLLECTION?.whereField("uid", isEqualTo: _userId).getDocuments(completion: {
(querySnapshot, error) in
if error != nil {
self.error = error
print(error?.localizedDescription as Any)
} else {
for document in querySnapshot!.documents {
userObject = User(snapShot: document)
completionHandler(userObject)
}
}
})
}
Could anyone tell me what I need to change for being able to fit the text data to the correct image in the cell?
With asynchronous calls to fetch user data, the fact that cells are re-used introduces two issues:
When a cell is re-used, make sure that you do not show the values for the prior cell while your asynchronous request is in progress. Either have collectionView(_:cellForItemAt:) reset the values or, better, have the cell’s prepareForReuse make sure the controls are reset.
In the asynchronous request completion handler, check to see if the cell is still visible before updating it. You do this by calling collectionView.cellForItem(at:). If the resulting cell is nil, then the cell is not visible and there's nothing to update.
Thus:
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let photoCell = collectionView.dequeueReusableCell(withReuseIdentifier: "mainViewCollectionCell", for: indexPath) as! MainViewCollectionViewCell
// make sure to initialize these so if the cell has been reused, you don't see the old values
photoCell.label.text = nil
photoCell.imageView.image = nil
// now in your asynchronous process completion handler, check to make sure the cell is still visible
someAsynchronousProcess(for: indexPath.row) {
guard let cell = collectionView.cellForItem(at: indexPath) else { return }
// update `cell`, not `photoCell` in here
}
return photoCell
}
Obviously, if one asynchronous completion handler initiates another asynchronous request, then you have to repeat this pattern.

How to use a completion block and then get the data into TableView cells?

I'm new to this so have just been learning about completion blocks, but I am unsure of how to do so in such a way that I get the data to then be apart of a tableview. I have seen other questions related, but regarding older versions of Swift.
I want the table view to contain all the fruit names collected from my database.
I have initialised an empty array list like so:
var fruitNames : [String] = []
Then fetch the data from my firestore database
func getNames(){
let db = Firestore.firestore()
db.collection("fruits").getDocuments() {(snapshot, error) in
if let error = error {
print("There was an error!")
} else {
for document in snapshot!.documents {
let name = document.get("name") as! String
self.fruitNames.append(name)
//completion needed
}
}
}
}
}
I have an extension added on for my tableView
extension FruitsViewController: UITableViewDataSource, UITableViewDelegate {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
fruitNames.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell")
cell?.textLabel?.text = self.fruitNames[indexPath.row]
return cell!
}
}
Inside your completion block, you need to tell your table view to update by calling the reloadData() method. In your class, you should have a variable holding your tableView. So, your getName should look like
func getNames(){
let db = Firestore.firestore()
db.collection("fruits").getDocuments() {(snapshot, error) in
if let error = error {
print("There was an error!")
} else {
for document in snapshot!.documents {
let name = document.get("name") as! String
self.fruitNames.append(name)
//completion needed
}
self.tableView.reloadData()
}
}
}
First of all, use [weak self] in closure. Otherwise it can lead to memory leak or crashes. You should read about
automatic reference counting and memory management
closures (https://docs.swift.org/swift-book/LanguageGuide/Closures.html)
If you want to display fruit names, you should call .reloadData() on your tableView object. Then, all delegate methods like numberOfRowsInSection or cellForRowAt will be called again.
You can do something like this :
You have to take an escaping closure as a parameter to the getName() method, which would return Void :
func getName(onComplition: #escaping (_ isSuccess: Bool, _ dataList: [String]) -> Void) {
let db = Firestore.firestore()
db.collection("fruits").getDocuments() {(snapshot, error) in
if let error = error {
print("There was an error!")
onComplition(true, [])
} else {
var data = [String]()
for document in snapshot!.documents {
let name = document.get("name") as! String
data.append(name) // Here data is local variable.
}
onComplition(true, data)
}
}
}
in ViewDidLoad()
override func viewDidLoad() {
self.getName { [weak self] (isSuccess, dataList) in
guard let weakSelf = self else { return }
weakSelf.fruitNames = dataList // fruitNames is your TableViewController's instance variable
weakSelf.tableView.reloadData()
}
}
I have written it directly in IDE, please ignore if there's any syntax error
If you have written perfect code to fetch fruit names.
But your table view is already initialized and loaded with default/empty items in the table view.
You have fetched data after the table view loaded.
So solution is you have to reload your table view again.
So in the closure (After fetching and appending your data to an array) just reload the table view like below and it reloads fresh data.
tableView.reloadData()
User [weak self] or [unowned self] for closure to avoid retain cycles and it causes memory issues.

Update UITableView Data from server - did not work

I am struggling with UITableView's data fetching and/or reloadData() from server. I am creating this app to check user's pronunciation via server. The data parsing came out well (I checked with print statement) but it won't update to my table cell.
I created a dictionary to store loaded Data:
var summaryDict = ["Overall Score" : "Score", "Words" : "Score", "Syllables": "Score", "Phonemes": "Score"]
var summaryArray = ["Overall Score", "Words", "Syllables", "Phonemes"]
I did also update dict values after parsing JSON data:
.responseJSON { response in
switch response.result {
case .success:
do{
let json = try JSON(data: response.data!)
if let data = response.data {
if let summaryData = self.parseJSON(data) {
DispatchQueue.main.async {
print(summaryData)
self.summaryDict["Overall Score"] = summaryData.summaryScore
self.summaryDict["Words"] = summaryData.wordScore
self.summaryDict["Syllables"] = summaryData.syllableScore
self.summaryDict["Phonemes"] = summaryData.phoneScore
print(self.summaryDict)
self.delegate?.didUpdateScore(self, score: summaryData)
}
}
}
let statusJson = json["status"].string
if statusJson == "success" {
completion("success")
}
else { completion("error parseJSON") }
} catch {
print(error.localizedDescription)
}
case .failure(let encodingError):
print("error:\(encodingError)")
}
}
Over my ViewController I did also added tableview datasource extension:
extension FreeTrialViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return audioSender.summaryDict.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "SummaryCell", for: indexPath) as! SummaryCell
cell.summaryLabel.text = self.audioSender.summaryArray[indexPath.row]
cell.summaryScore.text = self.audioSender.summaryDict[self.audioSender.summaryArray[indexPath.row]]
return cell
}
}
I tried to put reloadData() everywhere I can or DispatchQueue.main.async every now and then yet the cell did not update.
EDITED: Include delegate at view controller:
extension FreeTrialViewController: AudioSenderDelegate {
func didFailWithError(_ error: Error) {
print("parsing audio delegate error: \(error)")
}
func didUpdateScore(_ audioSender: AudioSender, score: SummaryData) {
// updateTable()
summaryTable.reloadData()
}
}
Here's the end result after multiple tries:
(when I took the picture I mistakenly deleted 1 char from the "Overall Score" from the array so the Overall Score disappeared but when I corrected it it goes for 4 "Score".
What I want in the table:
Overall Score: 97
Words: 96.8
Syllable: 96.9
Phonemes: 97.0
What really showed up:
EDITED:
I shall include here the func that I call out the table:
Pretty sure inside the finish recording is the parse data.
I did try adding the guard as:
guard audioSender.summaryDict["Words"] != "Score" else { return }
Yet it would come out blank.
It seems like I forgot to add the line:
audioSender.delegate = self
so the data won't reload.
Thanks so much for all of your help.
Such a shame on me :).

An Alamofire request for each cell of a table view

I want to populate a table view by parsing JSON data received thanks to several Alamofire requests. However, the thing is that each cell has a different request from the others. Each cell makes a request based on IndexPath. Since the calls are asynchronous, not all requests are achieved...
My code:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
var name = ""
let url = "https://........"+items[indexPath.section][indexPath.row]+"......"
Alamofire.request(url).responseJSON { (responseData) -> Void in
if((responseData.result.value) != nil) {
let swiftyJsonVar = JSON(responseData.result.value!)
let object = swiftyJsonVar["results"][0]
print(object)
name = object["name"].stringValue
cell.textLabel?.text = name
}
}
return cell
}
How would it be possible to achieve every request? Thanks in advance!
You should keep track of hooking indexpath with every request so you know what the value returned is for
struct Item
{
var indexPath:NSIndexPath!
func getData(index:NSIndexPath)
{
let url = "https://........"+items[indexPath.section][indexPath.row]+"......"
Alamofire.request(url).responseJSON { (responseData) -> Void in
if((responseData.result.value) != nil) {
let swiftyJsonVar = JSON(responseData.result.value!)
let object = swiftyJsonVar["results"][0]
globalArr// store indexpath and value
/// remove indexoath from allDownloads
/// send refresh to table
}
}
}
}
in cell for row
if(indexPath in globalArr)
{
// show it
}
else
{
// check whether it's download is in progress to avoid multiple same requests when scroll because of dequeuing
if(allDownloads doesn't contain inexpath)
{
// create object of item and start download
// allDownloads.add(object)
}
}
globalArr: keep track of all downloaded objects
allDownloads : keep track of in progress downloads

Why am I getting a tableView definition conflict?

I am taking an iOS programming class through udemy and am trying to build use the UITableViewDelegate functions to define and populate a table.
I am getting a "Definition conflicts with previous value" error, when calling cellForRowAtIndexPath, and it is referring to numberOfRowsInSection.
I am using Version 6.1.1 (6A2008a)
Any ideas?
import UIKit
class ViewController: UIViewController, UITableViewDelegate {
override func viewDidLoad() {
super.viewDidLoad()
//let's go ahead and define the URL that we are going to use. this literrally is saying, create a URL that Xcode can use using the string below.
let url0 = NSURL(string: "http://www.weather-forecast.com")
//let's create a task. the task will go to weather-forecast.com and get the contents.
//first, we define the task, and then specify the method that is associated with that task.
//we need to create an array of string values for the cities we are looking for that will be used in the task, as well as outside of the task.
var arrayCities: [String] = []
let task0 = NSURLSession.sharedSession().dataTaskWithURL(url0!) {
//NSURLSession opens an http session inside of the app.
//.dataTaskWithURL gets the data from the URL
//.dataTaskWithURL responds with three variables: data, response, error
(data, response, error) in
if error == nil {
//if error is nil, create a variable urlContent with type NSSTring, and encode data with NSUTF8StringEncoding
var urlContent = NSString(data: data, encoding: NSUTF8StringEncoding)
//let's then set up a sting that we can parse correctly
var stringParse = NSString(string: urlContent!)
//let's start parsing urlContent and putting it into an array for us to put into a table.
//let's see if the desired list of cities are indeed inside of the urlContent
if urlContent?.rangeOfString(">Paris Weather Forecast<") != nil{
arrayCities.append("Paris")
}
else{println("paris has not been added")}
if urlContent?.rangeOfString(">New York Weather Forecast<") != nil{
arrayCities.append("New York")
}
else {println("New York has not been added")}
if urlContent?.rangeOfString(">Dubai Weather Forecast<") != nil{
arrayCities.append("Dubai")
}
else {println("Dubai has not been added")}
if urlContent?.rangeOfString(">Rome Weather Forecast<") != nil{
arrayCities.append("Rome")
}
else {println("Rome has not been added")}
if urlContent?.rangeOfString(">Mumbai Weather Forecast<") != nil{
arrayCities.append("Mumbai")
}
else {println("Mumbai has not been added")}
if urlContent?.rangeOfString(">London Weather Forecast<") != nil{
arrayCities.append("London")
}
else {println("London has not been added")}
if urlContent?.rangeOfString(">Berlin Weather Forecast<") != nil{
arrayCities.append("Berlin")
}
else {println("Berlin has not been added")}
//println(stringParse)
//println(urlContent)
println(arrayCities)
println(arrayCities.count)}
}
//make sure we kick off task0.
//we do this by calling the resume() method
task0.resume()
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int{
return arrayCities.count
}
//the below code will be run eveytime for every amount of rows in the table.
//the mehtod defines the contents of each individual cell. you can put images and looks of the cells. it returns a UITable view cell, and all the content that goes with it.
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
//the variable indexPath tells you what row you are in everytime.
//let's create a cell to return. the reuseIdentifier is used to create a reference name for the cell.
//when we create and edit a prototype cell, we are referencing all the cell styles.
//if we had a situation where we had active users that appeared red in the table and active users that appeared green, we would need to set up two seperate Prototpye cells and two reuseIdentifiers.
let cell = UITableViewCell(style: UITableViewCellStyle.Default, reuseIdentifier: "cell")
cell.textLabel?.text = arrayCities[indexPath.row]
return cell
}
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
Please, paste your log error if none of the two propositions below didn't help.
Cell Identifier
In your ViewController view (storyboard), make sure you have a prototype cell. Select it and access its attributes. Make sure the cell identifier is the same that you use here :
let cell = UITableViewCell(style: UITableViewCellStyle.Default, reuseIdentifier: "cell")
Reload data
After you retrieved you data, you might want to reload the tableview :
self.tableView.reloadData()

Resources