strano
strano

Reputation: 181

Swift - TableView datasource from URLSession task

I have to populate a TableView with some data fetched with an URLSession task. The source is an XML file, so i parse it into the task. The result of parsing, is an array that i pass to another function that populate another array used by TableView Delegates.

My problem is that TableView Delegates are called before task ends, so tha table is empty when i start the app, unless a data reloading (so i know that parsing and task work fine).

Here is viewDidLoad function. listOfApps is my TableView

    override func viewDidLoad() {

            super.viewDidLoad() 

            fetchData()
            checkInstalledApps(apps: <ARRAY POPULATED>)  

            listOfApps.delegate = self
            listOfApps.dataSource = self

        }
    }

fetchData is the function where i fetch the XML file and parse it

    func fetchData() {
        let myUrl = URL(string: "<POST REQUEST>");
        var request = URLRequest(url:myUrl!)
        request.httpMethod = "POST"
        let postString = "firstName=James&lastName=Bond";
        request.httpBody = postString.data(using: String.Encoding.utf8);

        let task = URLSession.shared.dataTask(with: request) { (data: Data?, response: URLResponse?, error: Error?) in

            self.parser = XMLParser(data: data!)
            self.parser.delegate = self

        }
        task.resume()

    }

while checkInstalledApps is the function where i compose the array used by TableView Delegates.

    func checkInstalledApps(apps: NSMutableArray){
    ....
    installedApps.add(...)
    installedApps.add(...)
    ....
    }

So, for example, to set the number of rows i count installedApps elements

func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {

    if (installedApps.count == 0) {
        noApp = true
        return 1
    }
    return installedApps.count
}

that are 0. Obviously, if i reload data, it's all ok.

My problem is the async call: first of that i used an XML accessible via GET request, so i can use XMLParser(contentsOf: myUrl) and the time is not a problem. Maybe if the XML will grow up, also in this way i will have some trouble, but now i've to use a POST request

I've tried with DispatchGroup, with a

where group is let group = DispatchGroup(), but nothing.

So, how can i tell to the tableview delegate to wait the task response and the next function?

thanks in advance

Upvotes: 0

Views: 560

Answers (3)

Cristik
Cristik

Reputation: 32786

Your problem is caused by the fact that you currently have no way in knowing when the URLSession task ended. The reloadData() call occurs almost instantly after submitting the request, thus you see the empty table, and a later table reload is needed, though the new reload should be no sooner that the task ending.

Here's a simplified diagram of what happens: enter image description here

Completion blocks provide here an easy-to-implement solution. Below you can find a very simplistic (and likely incomplete as I don't have all the details regarding the actual xml parsing) solution:

func fetchData(completion: @escaping () -> Void) {
     let task = URLSession.shared.dataTask(with: request) { (data: Data?, response: URLResponse?, error: Error?) in
            self.parser = XMLParser(data: data!)
            self.parser.delegate = self
            completion()
}

override func viewDidLoad() {
    super.viewDidLoad() 
    fetchData() { [weak self] in self?.tableView.reloadData() }
}

Basically the completion block will complete the xml data return chain.

Upvotes: 0

zheck
zheck

Reputation: 298

The cleaner way is to use an empty state view / activityIndicator / loader / progress hud (whatever you want), informing the user that the app is fetching/loading datas,

After the fetch is done, just reload your tableview and remove the empty state view / loader

Upvotes: 0

Milan Nos&#225;ľ
Milan Nos&#225;ľ

Reputation: 19737

I would forget about DispatchGroup, and change a way of thinking here (you don't want to freeze the UI until the response is here).

I believe you can leave the fetchData implementation as it is.

In XMLParserDelegate.parserDidEndDocument(_:) you will be notified that the XML has been parsed. In that method call checkInstalledApps to populate the model data. After that simply call listOfApps.reloadData() to tell the tableView to reload with the new data.

You want to call both checkInstalledApps and listOfApps.reloadData() on the main thread (using DispatchQueue.main.async {}).

Also keep listOfApps.delegate = self and listOfApps.dataSource = self in viewDidLoad as it is now.

Upvotes: 1

Related Questions