Use Json data from network call in UITableView - ios

I am trying to use the data from my network call to display in the UItableview as cell names
Here is my current view controller
import UIKit
import Alamofire
import SwiftyJSON
class ViewController: UIViewController, UITableViewDataSource{
var articles = [Article]()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
//get data from network call
loaddata()
//end view did load
}
func loaddata(){
Alamofire.request(.GET, "http://ip-address/test.json")
.responseJSON { response in
// print(response.request) // original URL request
// print(response.response) // URL response
//print(response.data) // server data
//print(response.result) // result of response serialization
/*if let JSON = response.result.value {
print("JSON: \(JSON)")
}*/
//get json from response data
let json = JSON(data: response.data!)
//print(json)
//for loop over json and write all article titles articles array
for (key, subJson) in json["Articles"] {
if let author = subJson["title"].string {
let artTitle = Article(name: author)
self.articles.append(artTitle!)
}
/*if let content = subJson["content"].string {
// self.Content.append(content)
}*/
}
// print("\(self.titles)")
//print("\(self.Content[0])")
//print(self.articles)
//set variable to articles number 6 to check append worked
let name = self.articles[6].name
//print varibale name to check
print("\(name)")
}
}
func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
//let num = articles.count
// print(num)
//return number of rows
return articles.count
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = UITableViewCell()
//let (artTitle) = articles[indexPath.row]
// Fetches the appropriate article for the data source layout.
let article = articles[indexPath.row]
//set cell text label to article name
cell.textLabel?.text = article.name
return cell
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
//end of class
}
Here is the Article.swift file
class Article {
// MARK: Properties
var name: String
// MARK: Initialization
init?(name: String) {
// Initialize stored properties.
self.name = name
// Initialization should fail if there is no name or if the rating is negative.
if name.isEmpty {
return nil
}
}
}
I think I am almost there, here is what I can do so far
The network call with alamofire is working
I can get the article title using swiftJson
I then append the article title
I can then print from articles after the for loop so I know its working.
I just can't set it to the cell name
Is this because the UItableview is loaded before the data is loaded and hence article will be empty at that point?
Can you point me towards best practice for something such as this/ best design patterns and help me load data in

After you get the data from the request, call reloadData on your tableView on the main thread like so.
dispatch_async(dispatch_get_main_queue(), {
[unowned self] in
self.tableView.reloadData()
})
This will make the tableView refresh all of its contents, and your data should show up then.

Alamofire.request(.GET, Urls.menu).responseJSON { response in
if let jsonData = response.data {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0)) {
let json = JSON(data: jsonData)
//for loop over json and write all article titles articles array
newArticles = [Articles]()
for (key, subJson) in json["Articles"] {
if let author = subJson["title"].string {
if let artTitle = Article(name: author) {
newArticles.append(artTitle)
}
}
}
dispatch_async(dispatch_get_main_queue()) {
self.articles += newArticles
tableView.reloadData()
/*if let content = subJson["content"].string {
// self.Content.append(content)
}*/
}
// print("\(self.titles)")
//print("\(self.Content[0])")
//print(self.articles)
//set variable to articles number 6 to check append worked
let name = self.articles[6].name
//print varibale name to check
print("\(name)")
}
}
}

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 and Cells from Model Array

I'm stuck on something I don't quite understand as from a few tests, it looks like the generation of table cell is happening before but not as well after a page load and Alamofire request.
If you see below I'm trying to get it to where our museum's outbound shipments are viewed after referencing the pro:
import Alamofire
import SwiftyJSON
class ShipmentProSearchResultsTableViewController: UITableViewController {
var pronumber:String = ""
var shipments = [Shipment]()
typealias JSONStandard = [String: AnyObject]
override func viewDidLoad() {
super.viewDidLoad()
fetchShipments()
}
func fetchShipments() {
let parameters: Parameters = ["pro_number": pronumber]
let todoEndpoint: String = "OURHOST/shipments/api/details/pro"
Alamofire.request(todoEndpoint, method: .get, parameters: parameters)
.responseJSON { response in
if response.result.isSuccess{
let shipmentJSON : JSON = JSON(response.result.value!)
for (index, subJson):(String, JSON) in shipmentJSON{
let proNumber = subJson["proNumber"].int
let consigneeName = subJson["consignee"]["name"].string
let shipment = Shipment(proNumber: proNumber!, consigneeName: consigneeName!)
self.shipments.append(shipment)
}
print(self.shipments)
}else{
print("Could not get results")
}
}
}
// MARK: - Table view data source
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return shipments.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "ShipmentCell", for: indexPath)
let shipment = shipments[indexPath.row]
cell.textLabel?.text = "Hello"
cell.detailTextLabel?.text = "\(shipment.proNumber)"
return cell
}
}
Now where I printed the self.shipments, I get the following results:
[OakRidgeArchaeologicalRepositoryDispatcher.Shipment(proNumber: 471008276, consigneeName: "A1 CHICAGO INSTITUTE OF THE ARTS")]
So I know the data is appropriately being passed to the model. I will also note that the Table View Cell Identifier in the storyboard is correctly set to ShipmentCell. But after the query, nothing pops up in my table.
I'm using Swift 4.
You should reload the tableView after updating source field.
func fetchShipments() {
let parameters: Parameters = ["pro_number": pronumber]
let todoEndpoint: String = "OURHOST/shipments/api/details/pro"
Alamofire.request(todoEndpoint, method: .get, parameters: parameters)
.responseJSON { response in
if response.result.isSuccess{
let shipmentJSON : JSON = JSON(response.result.value!)
for (index, subJson):(String, JSON) in shipmentJSON{
let proNumber = subJson["proNumber"].int
let consigneeName = subJson["consignee"]["name"].string
let shipment = Shipment(proNumber: proNumber!, consigneeName: consigneeName!)
self.shipments.append(shipment)
}
tableView.reloadData() //<-------add this.
print(self.shipments)
}else{
print("Could not get results")
}
}
}

Assign value received from #escaping to instance variable in another class in swift

I believe that I misunderstood some conception in Swift and can assign received array to my instance variable. Can somebody explain why overall my announcementsList array has 0 elements?
UIViewController.swift
var announcementsList: [Announcement] = []
override func viewDidLoad() {
super.viewDidLoad()
api.getAnnouncements(){ announcements in //<- announcements is array which has 12 elements
for ann in announcements{
self.announcementsList.append(ann)
}
}
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return announcementsList.count //<- have 0 here
}
API.swift
func getAnnouncements(completion: #escaping ([Announcement]) -> ()){
var announcements: [Announcement] = []
let url = URL(string: "https://api.ca/announcements")!
let task = self.session.dataTask(with: url) {
data, response, error in
if let data = data,
let json = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] {
guard let announcements_json = json!["announcements"] as? [[String: Any]]
else { return }
for announcement in announcements_json{
let title = announcement["title"] as! String
let desc = announcement["description"] as! String
announcements.append(Announcement(title: title,desc: desc))
}
}
completion(announcements)
}
task.resume()
}
P.S.: In my defence, I should say code works in Java pretty well
UPD
In UIViewController.swift if glance inside my announcementsList in viewWillDisappear() I will get my objects there. So I assume that tableView() started count elements earlier then they became reassigned in viewDidLoad().
The question now how to assign objects inide viewDidLoad() to a new array faster than tableView() start count them.
var announcementsList: [Announcement] = [] {
didSet {
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
}
override func viewDidLoad() {
super.viewDidLoad()
api.getAnnouncements { announcements in
self.announcementsList = announcements
}
}
The operation is asynchronous , so the tableView reloads when the VC appears and at that moment the response isn't yet return
for ann in announcements{
self.announcementsList.append(ann)
}
self.tableView.reloadData()
BTW why not
self.announcementsList = announcements
self.tableView.reloadData()
Also don't know current thread of callback , so do
DispatchQueue.main.async {
self.announcementsList = announcements
self.tableView.reloadData()
}

Unable to append strings to array while parsing JSON data

I am having difficulties storing the results retrieved from a JSON source data. I have confirmed the ability to print the data retrieved but it was not able to store into my local array.
My end objective is to actually print in a UITableView the results.
Below is the code for my relevant table view controller :
import UIKit
class CommunityActivityTableViewController: UITableViewController {
var displayNameArr = [String]()
var postDateArr = [String]()
var postDetailArr = [String]()
var testArr = ["teaad"]
override func viewDidLoad() {
super.viewDidLoad()
parseJson()
print(self.displayNameArr.count) //returns 0
print(self.postDateArr.count) //returns 0
print(self.postDetailArr.count) //returns 0
print(self.testArr.count)
print("end")
}
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
// #warning Incomplete implementation, return the number of sections
return 1
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// #warning Incomplete implementation, return the number of rows
return self.displayNameArr.count
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
print("3")
let cell = tableView.dequeueReusableCellWithIdentifier("Cell_activity", forIndexPath: indexPath)
print("hi")
cell.textLabel?.text = "hi"
cell.detailTextLabel?.text = "test"
return cell
}
override func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
return UITableViewAutomaticDimension
}
override func tableView(tableView: UITableView, estimatedHeightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
return UITableViewAutomaticDimension
}
func makeAttributedString(title title: String, subtitle: String) -> NSAttributedString {
let titleAttributes = [NSFontAttributeName: UIFont.preferredFontForTextStyle(UIFontTextStyleHeadline), NSForegroundColorAttributeName: UIColor.purpleColor()]
let subtitleAttributes = [NSFontAttributeName: UIFont.preferredFontForTextStyle(UIFontTextStyleSubheadline)]
let titleString = NSMutableAttributedString(string: "\(title)\n", attributes: titleAttributes)
let subtitleString = NSAttributedString(string: subtitle, attributes: subtitleAttributes)
titleString.appendAttributedString(subtitleString)
return titleString
}
func parseJson(){
//MARK: JSON parsing
let requestURL: NSURL = NSURL(string: "<sanitised>")!
let urlRequest: NSMutableURLRequest = NSMutableURLRequest(URL: requestURL)
let session = NSURLSession.sharedSession()
let task = session.dataTaskWithRequest(urlRequest) {
(data, response, error) -> Void in
let httpResponse = response as! NSHTTPURLResponse
let statusCode = httpResponse.statusCode
if (statusCode == 200) {
print("Everyone is fine, file downloaded successfully.")
do{
let json = try NSJSONSerialization.JSONObjectWithData(data!, options:.AllowFragments)
if let results = json["result"] as? [[String: AnyObject]] {
for result in results {
if let lastname = result["last_name"] as? String {
if let postdate = result["timestamp"] as? String {
if let firstname = result["first_name"] as? String {
if let postdetails = result["post_details"] as? String {
let displayname = firstname + " " + lastname
//print(displayname)
self.displayNameArr.append(displayname)
self.postDateArr.append(postdate)
self.postDetailArr.append(postdetails)
self.testArr.append("haha")
}
}
}
}
}
}
}catch {
print("Error with Json: \(error)")
}
}
}
task.resume()}
}
As per the code above the print results of displaynamearr.count and postDateArr.count and postDetailArr.count returned 0 when it should have returned more than 0 as a result of parseJson() method.
I have printed the display name, postgame and post details variables and they all contain data within so the problem does not lie with the extraction of data but the appending of data into the array.
Appreciate any help provided thanks ! Developed on Xcode 7 and Swift 2.2
Sanitised my JSON source due to sensitive nature of information (i have verified the retrieval of information is OK)
dataTaskWithRequest() is an asynchronous data loading. It loads on the background thread ensuring your UI won't freeze up. So your array will be empty when you this will be getting executed and hence your error. You need to a completion handler like so:
func parseJson(completion: (isDone: Bool) -> ()){
///code
for result in results {
if let lastname = result["last_name"] as? String {
if let postdate = result["timestamp"] as? String {
if let firstname = result["first_name"] as? String {
if let postdetails = result["post_details"] as? String {
let displayname = firstname + " " + lastname
//print(displayname)
self.displayNameArr.append(displayname)
self.postDateArr.append(postdate)
self.postDetailArr.append(postdetails)
self.testArr.append("haha")
}
completion(isDone: True)
}
}
Now in viewDidLoad:
override func viewDidLoad() {
super.viewDidLoad()
parseJson(){ success in
if success{
print(self.displayNameArr.count) //returns a value
print(self.postDateArr.count) //returns a value
print(self.postDetailArr.count) //returns a value
print(self.testArr.count) //This wont because I havent added it in the completion handler
print("end")
self.tableView.reloadData()
}
}
}
All of your UI updates run on the main thread. If you do something like
let task = session.dataTaskWithRequest(urlRequest) {
(data, response, error) -> Void in
// ...
}.resume()
you start a task asynchronously on another thread (not the main thread). Your iPhone is doing a network request and this takes some time. So I guess when your cellForRowAtIndexPath delegate method is called you haven't received any data yet. This is the reason you don't see anything.
The easiest solution to this would be to reload the table view once you have received the data. When you're done with all the parsing in your parseJson method (outside of all the loops) simply run:
dispatch_async(dispatch_get_main_queue()) {
self.tableView.reloadData()
}
This forces your table view to update. Remember that you have to run code that updates the UI on the main thread. This is what dispatch_async(dispatch_get_main_queue()) {} does.
EDIT: The answer above was to illustrate the problem to you. The more elegant solution would be to use a completion handler like so:
func parseJson(completionHandler: (Bool) -> Void) {
//do all your json parsing.
//....
dispatch_asyc(dispatch_get_main_queue()) {
//run this if you received the data
//implement some kind of if statement that checks if the parsing was successful
completionHandler(true)
//run this if it failed
completionHandler(false)
}
}
In your viewDidLoad you would do something like
override func viewDidLoad() {
super.viewDidLoad()
//...
parseJson() { success in
tableView.reloadData()
if(success) {
print("success")
}
}
}
If you want to display an activity indicator while data is loaded (which I would recommend) it is easier to use a callback as I've just described.

Swift 2 data is not reloading data when the array of rows is modified

I'm just getting to grips with iOS development and Xcode altogether, and I'm learning it with Swift 2. I'm trying to get some JSON data from a URL, split it up into a swift Array, and display it in a TableView. I have managed to split the JSON data into an Array, but I'm having trouble reloading the table's data to get it to display this. Here's the code I have:
//
// ViewController.swift
// Table JSON
//
// Created by James Allison on 06/11/2015.
// Copyright © 2015 James Allison. All rights reserved.
//
import UIKit
class ViewController: UIViewController {
var cellContent = ["this should be replaced","something","something else"]
#IBOutlet weak var table: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
// construct url
let url = NSURL(string: "http://127.0.0.1:8888/json.php")!
let task = NSURLSession.sharedSession().dataTaskWithURL(url) { (data, response, error) -> Void in
// the following will happen when the task is complete
if let urlContent = data {
var webContent = NSString(data: urlContent, encoding: NSUTF8StringEncoding)
// remove []
webContent = webContent?.stringByReplacingOccurrencesOfString("[", withString: "")
webContent = webContent?.stringByReplacingOccurrencesOfString("]", withString: "")
// split by commas
var webContentArr = webContent?.componentsSeparatedByString(",")
var temp = ""
// remove quote marks
for var i = 0; i < webContentArr!.count; i++ {
temp = webContentArr![i]
temp = temp.stringByReplacingOccurrencesOfString("\"", withString: "")
webContentArr![i] = temp
}
print(webContentArr!)
self.cellContent = webContentArr! as! Array
self.table.reloadData()
}
else {
// something failed
print("Error: invalid URL or something.")
}
}
task.resume()
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return cellContent.count
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = UITableViewCell(style: UITableViewCellStyle.Default, reuseIdentifier: "Cell")
cell.textLabel?.text = cellContent[indexPath.row]
return cell
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
At the moment when I run it the table displays the original cellContent variable, but not the new one. No errors are produced, and the array is printed okay.
Edit: Thanks Joshua for your answer. I ended up using the following code to solve my issue:
dispatch_async(dispatch_get_main_queue(), { () -> Void in
self.cellContent = webContentArr! as! Array
self.table.reloadData()
})
At a guess, your "will happen when the task is complete" code is being run on some thread/queue other than main, which does not play well with UI updates. Anything that touches the UI must be done on the main queue.
You should adjust your code so that both the replacement of cellContent and the call to your table view to reloadData() are scheduled on the main queue after you're finished processing everything. To do so, wrap both the above-mentioned calls in an async dispatch sent to the main queue:
dispatch_async(dispatch_get_main_queue(), ^{
self.cellContent = webContentArr! as! Array
self.table.reloadData()
});
This will ensure the cellContent array isn't being modified "behind the table view's back" while it's updating the UI on the main queue (bad!) and that the table view doesn't try updating again until it's done with any ongoing updates.
I hope this helps.

Resources