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 :).
Related
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.
Recently got stuck on a problem of assigning freshly downloaded JSON data to table view datasource variable. I suppose the problem is something obvious but my skill is not enough to gather the big picture. Let me share a bunch of code.
(1) A function retrieves the data from Open Weather Map API (defined in the separate class 'GetWeather').
func getMowForecast(completion: #escaping ((WeatherForecast?, Bool)) -> Void) {
let url = URL(string: "http://api.openweathermap.org/data/2.5/forecast?id=524901&APPID=b3d57a41f87619daf456bfefa990fce4&units=metric")!
let request = URLRequest(url: url)
let task = URLSession.shared.dataTask(with: request) { data, response, error in
if let data = data {
do {
let json = try JSONDecoder().decode(WeatherForecast.self, from: data)
completion((json, true))
} catch {
print(error)
completion((nil, false))
}
} else {
print(error)
}
}
task.resume()
}
Everything works fine here. JSON loads correctly and fits the data model.
Here's a link to JSON data to be displayed in tableView: https://pastebin.com/KkXwxYgS
(2) A controller handles the display of retrieved JSON data in tableView format
import UIKit
class ForecastViewController: UITableViewController {
#IBOutlet weak var tableV: UITableView! // tableView outlet in the IB
let weatherGetter = GetWeather() // object to handle the JSON retrieval
var tableData: WeatherForecast? // tableView data source
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.tableData?.list.count ?? 0
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell") as! TableVCCell
cell.dateLabel.text = "\(self.tableData?.list[indexPath.row].dt)"
cell.tempLabel.text = "\(self.tableData?.list[indexPath.row].main.temp)"
cell.feelsLikeLabel.text = "\(self.tableData?.list[indexPath.row].main.feels_like)"
return cell
}
override func viewDidLoad() {
super.viewDidLoad()
navigationController?.navigationBar.prefersLargeTitles = true
tableV.delegate = self
tableV.dataSource = self
weatherGetter.getMowForecast { (data, status) in
if let data = data, status {
} else if status {
print("-------- Ошибка разбора данных прогноза погоды --------")
} else {
print("-------- Ошибка получения данных прогноза погоды --------")
}
self.tableData = data
print(self.tableData)
}
print(self.tableData?.list.count) // returns nil
self.tableData = weatherGetter.getMowForecast(completion: ((tableData, true))) // error - Cannot convert value of type '(WeatherForecast?, Bool)' to expected argument type '((WeatherForecast?, Bool)) -> Void'
}
}
The problem is - the table view gets nil datasource so it is unable to load the data and shows the blank screen.
I suppose the mistake is in scope - I try to retrieve the JSON data inside a function and it does not go anywhere else. What I am wondering about is - how comes that assigning the data to self.tableData does not makes any effect?
Could you please help.
Thank you!
Regards
First of all delete
print(self.tableData?.list.count) // returns nil
self.tableData = weatherGetter.getMowForecast(completion: ((tableData, true))) // error - Cannot convert value of type '(WeatherForecast?, Bool)' to expected argument type '((WeatherForecast?, Bool)) -> Void'
The error occurs because the method does not return anything and the completion handler syntax is wrong. Both lines are pointless anyway due to the asynchronous behavior of getMowForecast
Secondly I recommend to declare the data source array as a non-optional array of the type which represents List. Then you get rid of all those unnecessary optionals.
var tableData = [List]()
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.tableData.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell") as! TableVCCell
let weatherData = self.tableData[indexPath.row]
cell.dateLabel.text = "\(weatherData.dt)"
cell.tempLabel.text = "\(weatherData.main.temp)"
cell.feelsLikeLabel.text = "\(weatherData.main.feels_like)"
return cell
}
To be able to display the data – as already mentioned by others – you have to reload the table view in the completion handler. And assign the data only if status is true.
override func viewDidLoad() {
super.viewDidLoad()
navigationController?.navigationBar.prefersLargeTitles = true
tableV.delegate = self
tableV.dataSource = self
weatherGetter.getMowForecast { [weak self] (data, status) in
if let data = data, status {
self?.tableData = data.list
DispatchQueue.main.async {
self?.tableV.reloadData()
}
} else if status {
print("-------- Ошибка разбора данных прогноза погоды --------")
} else {
print("-------- Ошибка получения данных прогноза погоды --------")
}
}
}
And consider that the message Ошибка разбора данных прогноза погоды will be never displayed.
You need to reload the table inside the callback as it's asynchronous
self.tableData = data
print(self.tableData)
DispatchQueue.main.async { self.tableV.reloadData() }
I am trying to load some data into UITableView from a database but the delegate functions used to populate the tableView executes and returns an empty table before I have had a chance to retrieve the data to be displayed in tableView?
Please can someone advise?
Here is my code:
var arrayOptions = [String]()
override func viewDidLoad() {
super.viewDidLoad()
print("MenuOptions viewDidLoad ...")
getArrayOfOptionsForMenu()
}
private func getArrayOfOptionsForMenu(){
// Get list of menu options and populate array
// Construct parameters to send to server
var parameter = [String:String]()
parameter["getoptions"] = "formenu"
let optionsURL = LabBookAPI.getCredentialsUrl(parameters: parameter, targetUrl: "getOptions.php?")
var request = URLRequest.init(url: optionsURL)
request.httpMethod = "POST"
let task = session.dataTask(with: request) { (data, response, error) in
if let jsonData = data{
do{
let jsonObject = try JSONSerialization.jsonObject(with: jsonData, options: [])
print("jsonObject: \(jsonObject)")
guard
let myArray = jsonObject as? [String] else{
print("data not in [String] format")
return
}
DispatchQueue.main.async{
self.arrayOptions = myArray
print("self.arrayOptions.count: \(self.arrayOptions.count)")
}
}catch let error{
print("print error: \(error)")
}
}else if let requestError = error{
print("error detail: \(requestError)")
}else{
print("unexpected error")
}
}// End task
task.resume()
}// End of function
/* DELEGATE FUNCTIONS */
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
print("numberOfRowsInSection self.arrayOptions.count: \(self.arrayOptions.count)")
return self.arrayOptions.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
print("cellForRowAt self.arrayOptions.count: \(self.arrayOptions.count)")
let cell = UITableViewCell.init(style: .value1, reuseIdentifier: "optionCell")
cell.textLabel?.text = self.arrayOptions[indexPath.row]
print("self.arrayOptions[indexPath.row]: \(self.arrayOptions[indexPath.row])")
return cell
}
My console:
viewDidLoad ... NSDataPDFDocument: nil MenuOptions viewDidLoad ...
MenuOptions viewWillAppear ... numberOfRowsInSection
self.arrayOptions.count: 0 numberOfRowsInSection
self.arrayOptions.count: 0 numberOfRowsInSection
self.arrayOptions.count: 0 jsonObject: (
Profile,
"Sign out" ) self.arrayOptions.count: 2
Your data is fetched from the network which takes some time to do. Rendering the TableView doesn't need to wait around.
We call these longer tasks Asynchronous tasks, they run in the background and the app continues doing other things until we get a response.
All you need to do is tell the TableView to reload the data when you get your response
DispatchQueue.main.async {
self.arrayOptions = myArray
self.tableView.reloadData()
}
EDIT:
I think you are using a UITableViewController which already contains a UITableView property, so you can use self.tableView. If you aren't using a UITableViewController then you need to create an outlet and set self.tableView.dataSource = self and self.tableView.delegate = self
You just need to reload table data:
DispatchQueue.main.async{
self.arrayOptions = myArray
print("self.arrayOptions.count: \(self.arrayOptions.count)")
self.tableView.reloadData()
}
I am stuck in my code, I am trying show to API response tableview cell but i have not any idea how to fill data in array ,So not showing anything in my tableviewcell. I am using custome cell and Alamofire in swift. Please improve my mistake give me solution .
func Api_call()
{
let url = URL(string: "https://dousic.com/api/radiolist")!
let components = URLComponents(url: url, resolvingAgainstBaseURL: true)!
// let fragment = components.fragment!
print(components)
let params = ["user_id":"16" ]
Alamofire.request(url, method: .post, parameters: params, encoding: URLEncoding.default).responseJSON {response in
self.hideActivityIndicator()
var err:Error?
switch response.result {
case .success(let value):
print(value)
let json = JSON(value)
// returns nil if it's not an array
if let resData = json["radioList"].arrayObject
{
self.array_RadioList = resData as! [[String:AnyObject]]
}
if self.array_RadioList.count > 0 {
self.tbl_home.reloadData()
}
case .failure(let error):
err = error
print(err ?? "error .....")
}
}
}`
Thanks for help .
EDIT
Just create a radio list variable like this
var array_RadioList:[JSON]?
Get array from json like this
-
if let resData = json["response"]["radioList"].array {
self.array_RadioList = resData
self.tableView.reloadData()
}
and reload data.And get radio object in
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell: UITableViewCell? = self.tableView.dequeueReusableCell(withIdentifier: cellReuseIdentifier)
let radio:JSON? = array_RadioList?[indexPath.row]
cell?.textLabel?.text = radio?["radio_tags"].string
return cell ?? UITableViewCell()
}
If you are getting your array_RadioList from Api_call(), try this
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell : homeCell = tableView.dequeueReusableCell(withIdentifier: "homeCell")! as! homeCell
cell.lbl_name?.text = array_RadioList[indexPath.row]["radio_title"] as? String
return cell
}
and also check for numberOfRowsInSection function.
If the API you're calling is well-made, you should use a get method, not a post.
Also, I tried to use "https://dousic.com/api/radiolist?user_id=16" but it return
{
"response": {
"code": "301",
"error": "wrong url"
}
}
These 2 things could be your problem, or it could be in your custom cells, or in you cellforrow method...
If you can show more code it would help.
EDIT
Try to use this version of the optional chaining :
if let resData = json["radioList"].arrayObject as? [[String:AnyObject] {
self.array_RadioList = resData
self.tbl_home.reloadData()
}
and try to debug it with breakpoints to see if the application goes everywhere you want and what are your variables at this time.
Try this
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return [self.array_RadioList].count;
}
I'm trying to get search results to display on a tableView. I believe I have correctly parsed the JSON, the only problem is that the results won't display on my tableView.
Here is the code:
var searchText : String! {
didSet {
getSearchResults(searchText)
}
}
var itemsArray = [[String:AnyObject]]()
override func viewDidLoad() {
super.viewDidLoad()
self.tableView.delegate = self
self.tableView.dataSource = self
self.tableView.reloadData()
}
// MARK: - Get data
func getSearchResults(text: String) {
if let excapedText = text.stringByAddingPercentEncodingWithAllowedCharacters(NSCharacterSet.URLQueryAllowedCharacterSet()) {
Alamofire.request(.GET, "https://api.duckduckgo.com/?q=\(excapedText)&format=json")
.responseJSON { response in
guard response.result.error == nil else {
// got an error in getting the data, need to handle it
print("error \(response.result.error!)")
return
}
let items = JSON(response.result.value!)
if let relatedTopics = items["RelatedTopics"].arrayObject {
self.itemsArray = relatedTopics as! [[String:AnyObject]]
}
if self.itemsArray.count > 0 {
self.tableView.reloadData()
}
}
}
}
// MARK: - Table view data source
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 6 // itemsArray.count
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("SearchResultCell", forIndexPath: indexPath) as! SearchResultCell
if itemsArray.count > 0 {
var dict = itemsArray[indexPath.row]
cell.resultLabel?.text = dict["Text"] as? String
} else {
print("Results not loaded yet")
}
return cell
}
If I had a static API request I think this code would work because I could fetch in the viewDidLoad and avoid a lot of the .isEmpty checks.
When I run the program I get 6 Results not loaded yet (from my print in cellForRowAtIndexPath).
When the completion handler is called response in, it goes down to self.items.count > 3 (which passes) then hits self.tableView.reloadData() which does nothing (I checked by putting a breakpoint on it).
What is the problem with my code?
Edit
if self.itemsArray.count > 0 {
dispatch_async(dispatch_get_main_queue(), { () -> Void in
self.tableView.reloadData()
})
}
Tried this but the tableView still did not reload even though its reloading 6 times before the alamofire hander is called...
Here is the strange thing, obviously before the hander is called my itemsArray.count is going to be 0 so that's why I get Results not loaded yet. I figured out why it repeats 6 times though; I set it in numberOfRowsInSection... So #Rob, I can't check dict["Text"] or cell.resultLabel?.text because they're never getting called. "Text" is correct though, here is the link to the JSON: http://api.duckduckgo.com/?q=DuckDuckGo&format=json&pretty=1
Also, I do have the label linked up to a custom cell class SearchResultCell
Lastly, I am getting visible results.
Two problems.
One issue is prepareForSegue:
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
let searchResultTVC = SearchResultsTVC()
searchResultTVC.searchText = searchField.text
}
That's not using the "destination" view controller that was already instantiated, but rather creating a second SearchResultsTVC, setting its searchText and then letting it fall out of scope and be deallocated, losing the search text in the process.
Instead, you want:
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if let searchResultTVC = segue.destination as? SearchResultsTVC {
searchResultTVC.searchText = searchField.text
}
}
You shouldn't rely on didSet in the destination view controller to trigger the search, because that property is getting set by source view controller before the table view has even been instantiated. You do not want to initiate the search until view has loaded (viewDidLoad).
I would advise replacing the didSet logic and just perform search in viewDidLoad of that SearchResultsTVC.
My original answer, discussing the code provided in the original question is below.
--
I used the code originally provided in the question and it worked fine. Personally, I might streamline it further:
eliminate the rid of the hard coded "6" in numberOfRowsInSection, because that's going to give you false positive errors in the console;
the percent escaping not quite right (certain characters are going to slip past, unescaped); rather than dwelling on the correct way to do this yourself, it's better to just let Alamofire do that for you, using parameters;
I'd personally eliminate SwiftyJSON as it's not offering any value ... Alamofire already did the JSON parsing for us.
Anyway, my simplified rendition looks like:
class ViewController: UITableViewController {
var searchText : String!
override func viewDidLoad() {
super.viewDidLoad()
getSearchResults("DuckDuckGo")
}
var itemsArray: [[String:AnyObject]]?
func getSearchResults(text: String) {
let parameters = ["q": text, "format" : "json"]
Alamofire.request("https://api.duckduckgo.com/", parameters: parameters)
.responseJSON { response in
guard response.result.error == nil else {
print("error \(response.result.error!)")
return
}
self.itemsArray = response.result.value?["RelatedTopics"] as? [[String:AnyObject]]
self.tableView.reloadData()
}
}
// MARK: - Table view data source
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return itemsArray?.count ?? 0
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "SearchResultCell", for: indexPath) as! SearchResultCell
let dict = itemsArray?[indexPath.row]
cell.resultLabel?.text = dict?["Text"] as? String
return cell
}
}
When I did that, I got the following:
The problem must rest elsewhere. Perhaps it's in the storyboard. Perhaps it's in the code in which searchText is updated that you didn't share with us (which triggers the query via didSet). It's hard to say. But it doesn't appear to be a problem in the code snippet you provided.
But when doing your debugging, make sure you don't conflate the first time the table view delegate methods are called and the second time they are, as triggered by the responseJSON block. By eliminating the hardcoded "6" in numberOfRowsInSection, that will reduce some of those false positives.
I think you should edit :
func getSearchResults(text: String) {
if let excapedText = text.stringByAddingPercentEncodingWithAllowedCharacters(NSCharacterSet.URLQueryAllowedCharacterSet()) {
Alamofire.request(.GET, "https://api.duckduckgo.com/?q=\(excapedText)&format=json")
.responseJSON { response in
guard response.result.error == nil else {
// got an error in getting the data, need to handle it
print("error \(response.result.error!)")
return
}
let items = JSON(response.result.value!)
if let relatedTopics = items["RelatedTopics"].arrayObject {
self.itemsArray = relatedTopics as! [[String:AnyObject]]
// if have result data -> reload , & no if no
if self.itemsArray.count > 0 {
self.tableView.reloadData()
}
}else{
print("Results not loaded yet")
}
}
}
}
And
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("SearchResultCell", forIndexPath: indexPath) as! SearchResultCell
// i 'm sure: itemsArray.count > 0 in here if in numberOfRowsInSection return itemsArray.count
var dict = itemsArray[indexPath.row]
cell.resultLabel?.text = dict["Text"] as? String
return cell
}
And you should share json result(format) ,print dict in cellForRowAtIndexPath, so it s easy for help