Loading tableView after JSON only - json

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

Related

Type 'HeroStruct.Type' cannot conform to 'Decodable', how can i solve that?

import UIKit
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
var hero = [HeroStruct]()
#IBOutlet weak var tableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
tableView.delegate = self
tableView.dataSource = self
navigationController?.navigationBar.barTintColor = UIColor.systemYellow
navigationController?.navigationBar.titleTextAttributes = [.foregroundColor: UIColor.systemYellow ]
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 100
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = UITableViewCell(style: UITableViewCell.CellStyle.subtitle, reuseIdentifier: nil)
cell.textLabel?.text = ""
cell.backgroundColor = .systemYellow
return cell
}
func getJsonData(completion: #escaping () -> () ) {
let url = URL(string: "https://api.opendota.com/api/heroStats")
let task = URLSession.shared.dataTask(with: url!) { data, response, error in
if error != nil {
print(error?.localizedDescription)
}else {
do {
let result = try JSONDecoder().decode(HeroStruct.Type, from: data!)
}catch {
print(error)
}
}
}
}
}
import Foundation
struct HeroStruct : Decodable {
let localized_name : String
let primary_attr : String
let attack_type : String
let legs : Int
let img : String
}
First code block is my ViewController.swift page,
second code block is my HeroStruct.swift page,
I tried to get data from Json but i got error like this:
Type 'HeroStruct.Type' cannot conform to 'Decodable'
How can i solve this?
let result = try JSONDecoder().decode([HeroStruct].Type, from: data!)`
I tried write like this but doesn't work. Need help ,thanks.
Replace [HeroStruct].Type with [HeroStruct].self. Whenever you want to decode something, always use .self & not .Type.

How to pass a JSON decoded Array to another VC on swift

well Im trying to pass an array of articles which came from a news API to another VC so I will be able to display them on my TableView, now the problem is that the tableview is loaded before I updated my array, is there a way to update the array before the tableview is loaded?
MY CODE:
MY NewsViewController Code:
import UIKit
import Foundation
class NewsViewController: UITableViewController,NewsProtocol {
func didUpdateNewsArr(arr: [Articles]) {
newsArr = arr
print("I'm loaded first!!")
// print(newsArr[0].title)
}
var newsArr = [Articles]()
var newsManager = NewsManager()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
self.title = "Hot News"
newsManager.newsProtocolDelegate = self
newsManager.performRequest()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(true)
}
}
// MARK: - UITableView Delegate&DataSource Methods:
extension NewsViewController {
// Return the number of rows for the table.
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return newsArr.count
}
// Provide a cell object for each row.
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// Fetch a cell of the appropriate type.
let cell = tableView.dequeueReusableCell(withIdentifier: "newsCell", for: indexPath)
// Configure the cell’s contents.
cell.textLabel!.text = "Cell text"
return cell
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
}
}
my networking file:
import Foundation
import UIKit
protocol NewsProtocol {
func didUpdateNewsArr(arr: [Articles])
}
struct NewsManager {
let newsURL = "https://newsapi.org/v2/everything?q=apple&from=2020-10-19&to=2020-10-19&sortBy=popularity&apiKey=009a08a56d664f1b92986e9cce27767b"
var newsProtocolDelegate: NewsProtocol?
func performRequest() {
if let url = URL(string: newsURL) {
let session = URLSession(configuration: .default)
let task = session.dataTask(with: url) { (data, respone, error) in
if error != nil {
print(error)
return
}
if let safeData = data {
self.parseJSON(newsData: safeData)
}
}
task.resume()
}
}
func parseJSON(newsData: Data) {
let decoder = JSONDecoder()
do {
let decodedData = try decoder.decode(NewsData.self, from: newsData)
let articlesArr = decodedData.articles
newsProtocolDelegate?.didUpdateNewsArr(arr: articlesArr)
} catch {
print("")
}
}
}
I've solved the problem :) thanks fellas, all was needed was tableView.reload() to reload the cells, due to the fact that the JSON request took a while and the tableView was made up before the request, I've used DispatchQue :)

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

Displaying JSON imageURL into UITableView using Alamofire and SwiftyJSON

I am currently trying to display the JSON data on to my UITableView. I've created a separate class which handles and declares the data being extracted from the API. I also created a custom cell to display the data. However I am not sure how to display the images of the API. I was able to display the title of the API using just a standard TableViewController without custom cells:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "recipeCell", for: indexPath)
cell.textLabel?.text = recipes = [indexPath.row].title
return cell
}
but when trying to create a custom cell the image uses an imageURL which works differently. Other tutorials and demo don't seem to be using Alamofire and SwiftyJSON.
I did read the documentation: https://github.com/SwiftyJSON/SwiftyJSON#integration but still not sure how to solve this issue. Please tell me how you'd get the API data to display on my table.
Class
import Foundation
import SwiftyJSON
class Recipe {
var title: String!
var image: URL?
init(json: JSON) {
self.title = json["title"].stringValue
self.image = json["image"].url
}
}
CustomCell
import UIKit
class RecipeCell: UITableViewCell {
#IBOutlet weak var titleLabel: UILabel!
#IBOutlet weak var imgView: UIImageView?
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
}
ViewController
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
Alamofire.request(searchURL, method: .get, parameters: params, encoding: URLEncoding.default, headers: headers).response { [unowned self] response in
guard let data = response.data else { return }
let json = JSON(data: data)
for recipe in json.arrayValue {
let newRecipe = Recipe(json: recipe)
self.recipes.append(newRecipe)
}
for recipe in self.recipes {
print(recipe.title)
print(recipe.image)
}
}
}
// Display JSON Data into the tableView
func tableView( _ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return recipes.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "recipeCell", for: indexPath)
// What to write here?
return cell
}
Thank you in advance! :)
for this you have to make the extension of Imageview as
extension UIImageView {
func downloadedFrom(url: URL, contentMode mode: UIViewContentMode = .scaleAspectFit) {
contentMode = mode
URLSession.shared.dataTask(with: url) { (data, response, error) in
guard
let httpURLResponse = response as? HTTPURLResponse, httpURLResponse.statusCode == 200,
let mimeType = response?.mimeType, mimeType.hasPrefix("image"),
let data = data, error == nil,
let image = UIImage(data: data)
else { return }
DispatchQueue.main.async() { () -> Void in
self.image = image
}
}.resume()
}
func downloadedFrom(link: String, contentMode mode: UIViewContentMode = .scaleAspectFit) {
guard let url = URL(string: link) else { return }
downloadedFrom(url: url, contentMode: mode)
}
}
then you have to write cellForRowAt method as
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "recipeCell", for: indexPath)
let recipe = recipes[indexPath.row]
let imagePath = recipe.image
cell.titleLabel.text = recipe.title
cell. imgView.downloadedFrom(link: imagePath)
return cell
}

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