0

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

}
  • 3
    You either have to make the network request in your previous view controller and only navigate to the one containing the table view once the request finished execution or you can simply display a loading indicator until the data is gathered. Btw force unwrapping `data` in the completion handler of `URLSession.dataTask` is a really bad idea, since it can easily be `nil` in case there was a networking error. – Dávid Pásztor Dec 13 '18 at 14:48
  • 1
    Do not set the datasource delegate from interface builder, do `tableView.dataSource = self` after the load completes for your desired behaviour. Also, why is this a problem for you? A request can take a long time on weak connections, you should display an acitivty indicator ontop of tableview if you're waiting for data. Why delay the reload? – FruitAddict Dec 13 '18 at 14:56
  • @DávidPásztor Thanks for the reply. You're right about the data being forced unwrapped. Made the edit in the code. Loading indicator sounds like the best route. Thanks again. – Gustavo Picciuto Dec 13 '18 at 15:47
  • @FruitAddict is there a difference in performance or otherwise setting the delegate in IB rather than in code or viceversa ? – Gustavo Picciuto Dec 13 '18 at 15:49

3 Answers3

0

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.

Phil
  • 151
  • 9
0

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

Hendra Halim
  • 22
  • 1
  • 5
0

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 }

}