What is the convention when populating a tableview from JSON in swift? - json

I have seen someone create an object to receive the JSON data, then have an array of that object. and upon receiving new data from the JSON the Object array updates and the table view reloads.
How would i do this? I didn't really understand it, but i now need it as i need to receive data from PHP then parse it in Xcode onto a table view.
If you could, i would really be grateful if you could also show any optimisation tips.
I think they used the didSet variable in their code.
Thank you for reading!

This is what you meant with didSet right?
class ViewController: UIViewController, UITableViewDataSource {
#IBOutlet weak var myTableView: UITableView!
private var dataArray: [String] = [String]() {
didSet {
myTableView.reloadData()
}
}
override func viewDidLoad() {
super.viewDidLoad()
let jsonRequest: NSURLRequest = NSURLRequest(URL: NSURL(string: "yourEndPoint")!)
NSURLConnection.sendAsynchronousRequest(jsonRequest, queue: NSOperationQueue.mainQueue()) { (response: NSURLResponse!, data: NSData!, error: NSError!) -> Void in
if let jsonArray: [[String: String]] = NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions.MutableLeaves, error: nil) as? [[String: String]] {
for jsonObject in jsonArray {
if let stringFromKey: String = jsonObject["yourKey"] as String? {
self.dataArray.append(stringFromKey)
}
}
}
}
}
func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return dataArray.count
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell: UITableViewCell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as! UITableViewCell
cell.textLabel?.text = dataArray[indexPath.row]
return cell
}
}
You just need to exchange the JSON Parsing and Array type to fit your purpose

Related

UITableView returning nil when trying to call reloadData()

I am trying to get my table view to show json data from the news api. I've been able to parse the data and display it to the console but a nil value was caught in the self.tableview.reload(). I need help in resolving the issue
let urlRequest = "https://newsapi.org/v2/everythingq=Coronavirus&sortBy=publishedAt&apiKey"
var articles: [Articles]? = []
#IBOutlet weak var tableview: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
retriveData()
}
func retriveData(){
guard let aritcleUrl = URL(string: urlRequest) else {
return
}
let request = URLRequest(url: aritcleUrl)
let task = URLSession.shared.dataTask(with: request, completionHandler: { (data, response, error) -> Void in
if error != nil {
print(error ?? 0)
return
}
if let data = data {
self.articles = self.parseData(data: data)
// Reload table view
DispatchQueue.main.async {
self.tableview.reloadData()
}
}
})
task.resume()
}
func parseData(data:Data)-> [Articles] {
var articles: [Articles]? = []
do {
let jsonResult = try JSONSerialization.jsonObject(with: data, options: JSONSerialization.ReadingOptions.mutableContainers) as? NSDictionary
let jsonArticles = jsonResult?["articles"] as? [AnyObject] ?? []
for jsonArticle in jsonArticles{
let article = Articles()
article.author = jsonArticle["author"] as? String
article.title = jsonArticle["title"] as? String
article.publishedAt = jsonArticle["publishedAt"] as? String
articles?.append(article)
}
print(jsonArticles)
} catch {
print(error)
}
return articles ?? []
}
It seems that your class doesn't conform to table view's protocols.
You should edit your declaration to:
class <YourViewController>: UIViewController, UITableViewDelegate, UITableViewDataSource {
then in the viewDidLoad you should set the delegate and datasource to your tableview
override func viewDidLoad() {
super.viewDidLoad()
tableView.dataSource = self
tableView.delegate = self
retriveData()
}
Then you have to add the protocol stubs as suggested by xcode
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
self.articles?.count ?? 0
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// YOUR CELL CUSTOMIZATION GOES HERE
}
However I suggest you to look at any TableView tutorial on Internet, such as the Apple's official one
here

Get data from API call and show another viewController on UItableView row click

I'm new to Swift, so I ask for help. I show list of characters on tableview using API call.When user click one row, I want to call API ,get data and show it in new viewController
API call url looks like :
https://rickandmortyapi.com/api/character/{user_clicked_row_charactor_id}
Sample response for id = 3
{
"id": 3,
"name": "Summer Smith",
"status": "Alive",
"species": "Human",
"type": "",
"gender": "Female",
}
And so it is necessary for each of the characters. Tell or direct how to implement this?
Sorry for my bad English.
My code:
class UsersTableViewController: UITableViewController {
var characters = [Results]()
override func viewDidLoad() {
super.viewDidLoad()
LoadCharacters()
}
func LoadCharacters() {
let urlString = "https://rickandmortyapi.com/api/character/"
if let url = URL(string: urlString)
{
let session = URLSession(configuration: .default)
let task = session.dataTask(with: url) { (data, responce, error) in
if error != nil {
print(error!)
return
}
if let safeData = data {
self.parseJson(usersData: safeData)
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
}
task.resume()
self.tableView.reloadData()
}
}
func parseJson(usersData: Data) {
let decoder = JSONDecoder()
do {
let decodedData = try decoder.decode(JSONData.self, from: usersData)
characters = decodedData.results
print(decodedData.results[0].name)
} catch {
print(error)
}
}
struct JSONData: Decodable {
let results: [Results]
}
struct Results: Decodable {
let name: String
}
// MARK: - Table view data source
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return characters.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "user", for: indexPath)
let guys = characters[indexPath.row]
cell.textLabel?.text = guys.name
return cell
}
}
If I understand your question properly, you’d need to implement the didSelectRowAt of the table view method then pass the necessary data to the new view controller:
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let character = characters[indexPath.row]
let charactersViewController = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(identifier: "characterVC") as! CharacterDetailViewController
charactersViewController.character = character
present(charactersViewController.character, animated: true)
}
For this to work, you will need to have another view controller to show the character once tapped on. Give that view controller the Storyboard ID of "characterVC" in the Identity inspector. The view controller will also need a variable to receive the character object, perhaps:
class CharacterDetailViewController: UIViewController {
var character: Character?
override func viewDidLoad() {
super.viewDidLoad()
if let character = character {
// show the character on the view
}
}
}

Loading tableView after JSON only

I realize the tableView.reloadData() get's called in the JSON completion block to reload the tableView with the data received; I was wondering if there was a way to load the tableView only after this completion block has finished. What is happening is that the tableView first loads empty with default cells and a few seconds later the reloadData() method gets called inside the completion block and the tableView reloads and the data appears with the custom cells. I want to load the tableView ONLY when and after the data is received. What approach can I take? Is there a way to load the view only after this is completed? I basically don't want to have the user look at a blank table for a few seconds and wait for the data to appear. Here is my viewController code and simple structs to hold the model for the data.
viewController:
import UIKit
class ViewController: UIViewController, UITableViewDataSource {
var users = [User]()
#IBOutlet weak var tableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
guard let url = URL(string: "https://jsonplaceholder.typicode.com/users") else { return }
URLSession.shared.dataTask(with: url) { (data, response, error) in
do {
guard let data = data else { return }
let recieved = try JSONDecoder().decode([User].self, from: data)
self.users = recieved
DispatchQueue.main.async {
self.tableView.reloadData()
}
} catch let error as NSError {
print("Error: \(error.description)")
}
}.resume()
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return users.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! TableViewCell
cell.name.text = users[indexPath.row].name
cell.eMail.text = users[indexPath.row].email
return cell
}
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
}
Structs:
struct User: Decodable {
let id: Int
let name: String
let username: String
let email: String
let company: Company
}
struct Company: Decodable {
let name: String
let catchPhrase: String
let bs: String
}
Since you're essentially waiting for a network call before the data can be displayed, why not display a spinner or activity indicator on a view on top of the tableview then dismiss this when the data has been parsed successfully (or handle any errors). The alternative could be to request the data before the view is loaded in another class.
I think you can add activity indicator in your UITableView. So User will not see only blank UITableView. Or you can add background image in your UITableView, You can show it if the data is still empty and hide it after JSON decoded.
for reference background image in UITableView, you can see here
As per your suggestions; here is the route I took using an activityIndicator. I set a UIView onto of the tableView, then added an activityIndicator on top of that UIView and I also added a simple UILabel next to the activityIndicator with the string "Loading". I used propertyAnimator inside the JSON task after data had been received and after reloading the tableView, then stopping activityIndicator, fading out the UIView to show the tableView and then removing the UIView from the superView. Here is the code:
import UIKit
class ViewController: UIViewController, UITableViewDataSource {
var users = [User]()
#IBOutlet weak var tableView: UITableView!
#IBOutlet weak var loadingView: UIView!
#IBOutlet weak var activityIndicator: UIActivityIndicatorView!
override func viewDidLoad() {
super.viewDidLoad()
guard let url = URL(string: "https://jsonplaceholder.typicode.com/users") else { return }
URLSession.shared.dataTask(with: url) { (data, response, error) in
do {
guard let data = data else { return }
let recievedUsers = try JSONDecoder().decode([User].self, from: data)
self.users = recievedUsers
DispatchQueue.main.async {
self.tableView.reloadData()
if self.loadingView.alpha == 1.0 {
self.activityIndicator.stopAnimating()
UIViewPropertyAnimator.runningPropertyAnimator(withDuration: 1.0,
delay: 0.0,
options: [],
animations: {
self.loadingView.alpha = 0.0
},
completion: { (position) in
if position == .end {
self.loadingView.removeFromSuperview()
}
})
}
}
} catch let error as NSError {
print("Error: \(error.description)")
}
}.resume()
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return users.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! TableViewCell
cell.name.text = users[indexPath.row].name
cell.eMail.text = users[indexPath.row].email
return cell
}
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
}

Table empty when populating cells from json

I am trying to populate a table with json content. Everything seems to work fine except that the table is not showing any data. Actually, the code shown below should display the "title" information of each json data array into one cell. See line
cell.textLabel?.text = myNewsItems[indexPath.row].title
However, from what I can see in the console output, I can verify that the news array is parsed like expected (see Checkpoint: print(myNewsS)).
Any idea what I am missing?
Swift4
import UIKit
// structure from json file
struct News: Codable{
let type: String
let timestamp: String
let title: String
let message: String
}
class HomeVC: UIViewController, UITableViewDelegate, UITableViewDataSource{
var myTableView = UITableView()
var myNewsItems: [News] = []
override func viewDidLoad() {
super.viewDidLoad()
let barHeight: CGFloat = UIApplication.shared.statusBarFrame.size.height
let displayWidth: CGFloat = self.view.frame.width
let displayHeight: CGFloat = self.view.frame.height
myTableView = UITableView(frame: CGRect(x: 0, y: 150, width: displayWidth, height: displayHeight - barHeight))
myTableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
myTableView.dataSource = self
myTableView.delegate = self
self.view.addSubview(myTableView)
// JSON
let url=URL(string:"https://api.myjson.com/bins/ywv0k")
let session = URLSession.shared
let task = session.dataTask(with: url!) { (data, response, error) in
// check status 200 OK etc.
guard let data = data else { return }
do {
let myNewsS = try
JSONDecoder().decode([News].self, from: data)
print(myNewsS)
DispatchQueue.main.async {
self.myTableView.reloadData()
}
} catch let jsonErr {
print("Error json:", jsonErr)
}
}
task.resume()
}
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return myNewsItems.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
cell.textLabel?.text = myNewsItems[indexPath.row].title
return cell
}
}
Assign the array
myNewsItems = myNewsS
DispatchQueue.main.async {
self.myTableView.reloadData()
}

How to load JSON into TableView?

I am trying to load an exercises JSON into a table view, i have a service function that gets the data from a source as JSON, and a view controller with a table that I want to load the info into. There are no errors in the code however the table loads blank rows, the debug section shows the JSON data just fine via a print command. Im a beginner so im sure im missing a core element, but cant work it out!
api service
class ApiService {
static var swiftyJsonVar:JSON?
class func getExerciseData() {
Alamofire.request("https://wger.de/api/v2/exercise/?format=json").responseJSON { (responseData) -> Void in
if((responseData.result.value) != nil) {
swiftyJsonVar = JSON(responseData.result.value!)
print(swiftyJsonVar ?? nil)
}
}
}
View Controller
class ExerciseDatabaseController: UIViewController, UITableViewDataSource, UITableViewDelegate {
#IBOutlet weak var ExerciseSearchField: UISearchBar!
#IBOutlet weak var ExercisesTableView: UITableView!
var arrRes = [[String:AnyObject]]() // Array of dictionary
override func viewDidLoad() {
super.viewDidLoad()
let arrRes = ApiService.getExerciseData()
if let resData = ApiService.swiftyJsonVar?["exercise"].arrayObject {
self.arrRes = resData as! [[String:AnyObject]]
}
if self.arrRes.count > 0 {
self.ExercisesTableView.reloadData()
}
print(arrRes)
// Do any additional setup after loading the view.
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return arrRes.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
var dict = arrRes[indexPath.row]
cell.textLabel?.text = dict["name"] as? String
cell.detailTextLabel?.text = dict["description"] as? String
return cell
}
You should be loading your JSON asynchronously, which means you should have a closure in the method that makes your alamofire call.
class ApiService {
class func getExerciseData(completion: #escaping ([[String: AnyObject]]) -> ()) {
Alamofire.request("https://wger.de/api/v2/exercise/?format=json").responseJSON { (responseData) -> Void in
guard let jsonResponse = responseData.result.value else {
//possibly put some sort of protection, or what you want to do if there is not a response here
return
}
//instead of creating a variable for swiftyJsonVar in this class,
//you want to use a completion to send the array of dictionaries to the tableview asynchronously,
//that way it doesn't load blank
//I'm not super familiar with swifty json(sorry). What I normally do is below.
let swiftyJsonVar = JSON(jsonResponse)
guard let dictArray = swiftyJsonVar["exercise"].arrayObject as? [[String: AnyObject]] else {
//some sort of protection here if this fails
return
}
completion(dictArray)
}
}
So now we have made our asynchronous call(generally you want to do this whenever you are displaying information visually from an internet call that was not already preloaded/saved somewhere in app).
Next, we want to display this information in our tableview upon tableview load.
class ExerciseDatabaseController: UIViewController, UITableViewDataSource, UITableViewDelegate {
//these should start with lower cases(exerciseSearchField), never uppercased
#IBOutlet weak var ExerciseSearchField: UISearchBar!
#IBOutlet weak var ExercisesTableView: UITableView!
var arrRes = [[String:AnyObject]]() // Array of dictionary
override func viewDidLoad() {
super.viewDidLoad()
//you said you would use these delegates up top when you created the class, so you have to set them
ExercisesTableView.delegate = self
ExercisesTableView.dataSource = self
fetchData()
// Do any additional setup after loading the view.
}
//this method will make the api call
//you'll notice that if you set breakpoints, it will reach the end of the method before hitting self?.arrRes = dictArray
//this is normal and how asynchronous calls work, look into tableview threading for a deeper explanation of why that is. It is super important to understand threading in iOS
//once it gets data back from the API call, it will go to the main thread and tell the tableview to reload with that data
func fetchData() {
ApiService.getExerciseData { [weak self] (dictArray) in
self?.arrRes = dictArray
print(self?.arrRes)
if self?.arrRes.count > 0 {
DispatchQueue.main.async {
self?.ExercisesTableView.reloadData()
}
}
}
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return arrRes.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
var dict = arrRes[indexPath.row]
cell.textLabel?.text = dict["name"] as? String
cell.detailTextLabel?.text = dict["description"] as? String
return cell
}
You'll see I used [weak self] above. For more of an explanation of why that is necessary with asynchronous internet calls/whenever using closures, you can read here:
http://krakendev.io/blog/weak-and-unowned-references-in-swift
There are a lot of other resources for reading about weak and strong references/parent child stuff in iOS with a quick google search. Also, pursue researching asynchronous/synchronous in iOS. Both of these topics are incredibly important to learn when beginning.
Reload your tableView once the JSON data from your asynchronous request is received. So your
self.ExercisesTableView.reloadData()
will go inside
Alamofire.request("https://wger.de/api/v2/exercise/?format=json").responseJSON { (responseData) -> Void in
if((responseData.result.value) != nil) {
swiftyJsonVar = JSON(responseData.result.value!)
print(swiftyJsonVar ?? nil)
}
}