Meri
Meri

Reputation: 67

UITableView got displayed before data from API using Codable

enter image description here

When I run the application I can see a blank table like the below screenshot loaded for certain milliseconds and then loading the table with actual data.As the items array is having 0 elements at the beginning, numberOfRowsInSection returns 0 and the blank table view is loading. Is it like that? Please help me on this.

func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return self.items.count
}

I changed above code to the one below, but same issue exists and in debug mode I found out that the print("Item array is empty") is executing twice, then the blank table view is displaying for a fraction of seconds, after that the actual API call is happening and data is correctly displayed in the tableview.

func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    if items.isEmpty{
        print("Item array is empty")
        return 0
    } else {
        return self.items.count
    }
}
import UIKit
class MainVC: UIViewController,UITableViewDelegate,UITableViewDataSource {

    @IBOutlet weak var bookslideShow: UIView!
    @IBOutlet weak var bookTableView: UITableView!
    var items : [Items] = []

    override func viewDidLoad() {

        super.viewDidLoad()
        bookTableView.dataSource = self
        bookTableView.delegate = self
        self.view.backgroundColor = UIColor.lightGray
        bookTableView.rowHeight = 150
        // self.view.backgroundColor = UIColor(patternImage: UIImage(named: "background.jpeg")!)

        self.fetchBooks { data in
            self.items.self = data
            DispatchQueue.main.async {
                self.bookTableView.reloadData()
            }
        }
    }
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        if items.isEmpty{

            print("Item array is empty")
            return 0

        } else {
            return self.items.count
            //bookTableView.reloadData()
        }
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "BookCell",for:indexPath) as! BookCell
        //cell.backgroundColor = UIColor(red: 180, green: 254, blue: 232, alpha: 1.00)
        let info = items[indexPath.row].volumeInfo
        cell.bookTitle.text = info.title
        cell.bookCategory.text = info.categories?.joined(separator: ",")
        cell.bookAuthor.text = info.authors?.joined(separator: ", ")
        let imageString = (info.imageLinks?.thumbnail)!


        if let data = try? Data(contentsOf: imageString) {
            if let image = UIImage(data: data) {
                DispatchQueue.main.async {
                    cell.bookImage.image = image
                }
            }
        }
        return cell
    }

    func fetchBooks(comp : @escaping ([Items])->()){

        let urlString = "https://www.googleapis.com/books/v1/volumes?q=quilting"
        let url = URL(string: urlString)

        guard url != nil else {
            return
        }
        let session = URLSession.shared

        let dataTask = session.dataTask(with: url!) { [self] (data, response, error) in
            //check for errors
            if error == nil && data != nil{
                //parse json

                do {
                    let result = try JSONDecoder().decode(Book.self, from: data!)
                    comp(result.items)
                }
                catch {
                    print("Error in json parcing\(error)")
                }
            }
        }
        //make api call
        dataTask.resume()
    }
}

Upvotes: 0

Views: 837

Answers (4)

After making sure that you have the reloadData() called, make sure your constraints for labels/images are correct. This makes sure that you're labels/images can be seen within the cell.

Upvotes: 0

Zhou Haibo
Zhou Haibo

Reputation: 2078

Normally I will do data task as below code show, please see the comments in code.

// show a spinner to users when data is loading
self.showSpinner()
DispatchQueue.global().async { [weak self] in
    
    // Put your heavy lifting task here,
    // get data from some completion handler or whatever
    loadData()

    // After data is fetched OK, push back to main queue for UI update
    DispatchQueue.main.async {
        self?.tableView.reloadData()
        // remove spinner when data loading is complete
        self?.removeSpinner()
    }
}

Upvotes: 0

SeaSpell
SeaSpell

Reputation: 758

You are crossing the network for the data. That can take a long time especially if the connection is slow. An empty tableview isn't necessarily bad if you are waiting on the network as long as the user understands what's going on. Couple of solutions,

  1. Fetch the data early in application launch and store locally. The problem with this approach is that the user may not ever need the downloaded resources. For instance if instantgram did that it would be a huge download that wasn't needed for the user. If you know the resource is going to be used entirely get it early or at least a small part of it that you know will be used.

2)Start fetching it early even before the segue. In your code you need it for the table view but you're waiting all the way until view did load. This is pretty late in the lifecycle.

3)If you have to have the user wait on a resource let them know you're loading. Table View has a refresh control that you can call while you are waiting on the network or use a progress indicator or spinner. You can even hide your whole view and present a view so the user knows what's going on.

Also tableview is calling the datasource when it loads automatically and you're calling it when you say reloadData() in your code, that's why you get two calls.

So to answer your question this can be accomplished any number of ways, you could create a protocol or a local copy of the objects instance ie: MainVC in your presentingViewController then move your fetch code to there and set items on the local copy when the fetch comes back. And just add a didset to items variable to reload the tableview when the variable gets set. Or you could in theory at least perform the fetch block in the segue passing the MainVC items in the block.

For instance

 override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
    let vc = segue.destination as MainVC
     
    self.fetchBooks { data in
        vc.items.self = data // not sure what the extra self is?
        DispatchQueue.main.async {
            vc.bookTableView.reloadData()
        }
    }
    
 }

Since the closure captures a strong pointer you can do it this way.

Upvotes: 0

RTXGamer
RTXGamer

Reputation: 3750

The delegates methods may be called multiple times. If you want to remove those empty cells initially. You can add this in viewDidLoad:

bookTableView.tableFooterView = UIView()

Upvotes: 0

Related Questions