Swift: Populate UITableView with JSON data? - ios

I've looked around for days, and can't figure out how to populate a UITableView with JSON data. I'm appending values to a recommendedTitles array and a recommendedThumbnails array, which I can associate between the two using an index.
I'm not getting any errors, just not getting any results. Am I missing something or doing this incorrectly?
class RecommendationCell: UITableViewCell {
#IBOutlet weak var recommendationTitle: UILabel!
}
class SongViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
var recommendedTitles = [String]()
var recommendedImages = [String]()
override func viewDidLoad() {
super.viewDidLoad()
let url = NSURL(string: "https://www.googleapis.com/youtube/v3/search?part=id&q=\(searchTerm)&maxResults=1&key=AIzaSyD7PxAoq0O5hUxx775l_E_mnowlU4cUfcI")
let task = NSURLSession.sharedSession().dataTaskWithURL(url!, completionHandler: {data, response, error -> Void in
if error != nil {
println(error)
} else {
let jsonResult = NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions.MutableContainers, error: nil) as! NSDictionary
if let items = jsonResult["items"] as? NSArray {
// yada yada yada...
self.recommendedTitles.append(title)
self.recommendedImages.append(thumbnail)
})
newTask.resume()
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return recommendedTitles.count
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var songCell = tableView.dequeueReusableCellWithIdentifier("songCell", forIndexPath: indexPath) as! RecommendationCell
songCell.recommendationTitle.text = recommendedTitles[indexPath.row]
return songCell
}
}

Your code looks reasonable. The one thing I notice is that you're not calling your tableView's reloadData() method once the JSON is done downloading.
Here's what I think is going on:
You start an async download in your viewDidLoad. That returns immediately, before the data has downloaded.
Your table view calls it's data source methods to see how many sections of data and rows/section are available. It is told there is no data.
Then your completion code fires, but the table view still thinks there's no data. If you add a reloadData() call at the end of your completion block, the table view will call your data source methods again, discover that there is now data to display, and draw your contents.

Related

Why JSON data from decoder to become UITableView datasource does not assigns?

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() }

UITableView Delegate functions execute before data source can be retrieved from database - SWIFT?

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()
}

Dynamically populating an iOS table view with Swift from an API

I'm currently in the process of creating an app to display the latest football scores. I've connected to an API through a URL and pulled back the team names for the english premier league into an array of strings.
The problem seems to come from populating the iOS table view that I intend to display the list of teams with. The data appears to be pulled from the API fine, but for some reason the TableView method which creates a cell and returns it doesn't seem to be called. The only time I can get the method to be called is when I actually hard code a value into the array of team names.
Here is my code:
class Main: UIViewController {
var names = [String]()
override func viewDidLoad() {
super.viewDidLoad()
let URL_String = "https://football-api.com/api/?Action=standings&APIKey=[API_KEY_REMOVED]&comp_id=1204"
let url = NSURL(string: URL_String)
let urlRequest = NSURLRequest(URL: url!)
let config = NSURLSessionConfiguration.defaultSessionConfiguration()
let session = NSURLSession(configuration: config)
let task = session.dataTaskWithRequest(urlRequest, completionHandler: {
(data, response, error) in
do {
let json = try NSJSONSerialization.JSONObjectWithData(data!, options: .AllowFragments)
if let teams = json["teams"] as? [[String : AnyObject]] {
for team in teams {
if let name = team["stand_team_name"] as? String {
self.names.append(name)
}
}
}
} catch {
print("error serializing JSON: \(error)")
}
})
task.resume()
}
// Number of Sections In Table
func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1
}
// Number of Rows in each Section
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return names.count
}
// Sets the content of each cell
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath)
cell.textLabel?.text = names[indexPath.row]
return cell
}
}
Just wondering if anyone can point me in the right direction here. This code doesn't crash or throw any errors, it just refuses to load a table view. The only reason I can possibly think of is that the array of team names is empty after completing a request to the API. However I've set breakpoints throughout and checked the values of local variables and the desired information is being pulled from the API as intended...
you are in the correct way , just refresh the table using reloadData once you got the new data from API
if let teams = json["teams"] as? [[String : AnyObject]] {
for team in teams {
if let name = team["stand_team_name"] as? String {
self.names.append(name)
}
}
dispatch_async(dispatch_get_main_queue(), { () -> Void in
self.yourtableViewname.reloadData()
})
}

Data does not show up in table view cell when i run my code

Please help! I've tried everything. If anyone has any advice on how i can display my data in the table view cell, I would be eternally grateful. I'm new to iOS and am learning on a very steep pace. I grabbed data from an API that returned data in the form of JSON, parsed it, created my table view with its table view cells, but i can't seem to figure out how to print the data i parsed through in the table view cell.
import UIKit
class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
#IBOutlet weak var myTableView: UITableView! {
didSet {
myTableView.dataSource = self
myTableView.delegate = self
}
}
override func viewDidLoad() {
super.viewDidLoad()
let url = NSURL(string: "https://api.viacom.com/apiKey=someKey")!
let request = NSMutableURLRequest(URL: url)
let session = NSURLSession.sharedSession()
let task = session.dataTaskWithRequest(request) { data, response, error in
if let response = response, data = data {
var json: [String: AnyObject]!
do {
json = try NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions()) as! [String : AnyObject]
} catch {
print(error)
}
//2 - Store in model, forloop through them, store into temparray,add to main array?
let episodes = json["response"] as! [String: AnyObject]
let meta = episodes["episodes"] as! [AnyObject]
let description = meta[2]["description"]! as! String?
//let title = meta[2]["title"] as! String?
let episodeNumber = meta[2]["episodeNumber"]! as! String?
dispatch_async(dispatch_get_main_queue(), {
self.myTableView.reloadData()})
data = [episodeNumber!, description!]
print("Episode Number: \(episodeNumber!)\n" + "Description: \(description!)")
} else {
print(error)
}
}
task.resume()
}
let data = [description]
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 1
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("cell", forIndexPath: indexPath)
cell.textLabel!.text = "\(self.data)"
return cell
}
override func didReceiveMemoryWarning()
{
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
Your codes look very messy to me. However, I'm just assuming that you have successfully fetched the JSON data. Fetching data is asynchronous. You therefore need to add a dispatch code inside.
After your this line of code:
let episodeNumber = meta[2]["episodeNumber"]! as! String?
Add this
dispatch_async(dispatch_get_main_queue(), {
self.tableView.reloadData()})
EDIT:
#IBOutlet weak var myTableView: UITableView! {
didSet {
myTableView.dataSource = self
myTableView.delegate = self // Add This
}
}
The reason for the failure is too much of data manipulation. There is no need to use so many variables and pass around data unnecessarily. You are getting correct output in console when printing it because you used variables "episodeNumber" and "description".
print("Episode Number: \(episodeNumber!)\n" + "Description: \(description!)")
And getting wrong data in variable "data".
So better thing would be that you should use episodeNumber and description variables to print data in Cell.
cell.textLabel!.text = "Episode Number: \(self.episodeNumber)\n" + "Description: \(description)"
But for this you have to make variable episodeNumber a global variable.
So declare it outside the function.
var episodeNumber = String()
and remove the let keyword from line
let episodeNumber = meta[2]["episodeNumber"]! as! String?
You have to add some self. keywords which the compiler will suggest you so you don't have to worry about that, just keep on double clicking the suggestions.
Now, your code looks fine to run and get desired output.
let data = [description]
is a short form of
let data = [self.description]
and self.description() is the viewController's description method used for printing debug description. That is why
cell.textLabel!.text = "\(self.data)"
gives you [(Function)], as you just created an array with a stored function in it.

Populating table with API response Swift

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

Resources