59

I'm attempting to learn Swift & the basics of iOS dev at the same time, so bear with me. I've got a TableViewController that is firstly parsing a local JSON file and rendering it's very simple data into TableViewCell and SectionHeaderViews. Within the same TableViewController, I'm making a call to a JSON endpoint, which is returning data, which I am then setting to variables so I can access what I actually want to get at (the API structure is less than desirable). So, I finally set the proper data to be self.tableData and then call self.tableView.reloadData() but nothing happens. What gives?

import UIKit

class BusinessTableViewController: UITableViewController {

    var data: NSMutableData = NSMutableData()
    var tableData: NSArray = NSArray()

    @lazy var Business: NSArray = {
        let pathTCT = NSBundle.mainBundle().pathForResource("TCT", ofType: "json")
        let data = NSData.dataWithContentsOfFile(pathTCT, options: nil, error: nil)
        return NSJSONSerialization.JSONObjectWithData(data, options: nil, error: nil) as NSArray
        }()

    override func viewDidLoad() {
        super.viewDidLoad()

        navigationItem.titleView = UIImageView(image: UIImage(named: "growler"))

        tableView.registerClass(BeerTableViewCell.self, forCellReuseIdentifier: "cell")
        tableView.separatorStyle = .None

        fetchKimono()
    }

    override func numberOfSectionsInTableView(tableView: UITableView!) -> Int {
//        return Business.count
        return 1
    }

    override func tableView(tableView: UITableView?, numberOfRowsInSection section: Int) -> Int {
        let biz = Business[section] as NSDictionary
        let results = biz["results"] as NSDictionary
        let beers = results["collection1"] as NSArray
        return beers.count
    }

    override func tableView(tableView: UITableView?, cellForRowAtIndexPath indexPath: NSIndexPath?) -> UITableViewCell? {
        let cell = tableView!.dequeueReusableCellWithIdentifier("cell", forIndexPath: indexPath!) as BeerTableViewCell
        if let path = indexPath {
            let biz = Business[path.section] as NSDictionary
            let results = biz["results"] as NSDictionary
            let beers = results["collection1"] as NSArray
            let beer = beers[path.row] as NSDictionary

            cell.titleLabel.text = beer["BeerName"] as String
        }

        return cell
    }

    override func tableView(tableView: UITableView!, titleForHeaderInSection section: Int) -> String! {
        let biz = Business[section] as NSDictionary
        return biz["name"] as String
    }

    override func tableView(tableView: UITableView!, viewForHeaderInSection section: Int) -> UIView! {
        let biz = Business[section] as NSDictionary
        let view = LocationHeaderView()
        view.titleLabel.text = (biz["name"] as String).uppercaseString
        return view
    }

    override func tableView(tableView: UITableView!, heightForHeaderInSection section: Int) -> CGFloat {
        return 45
    }

    func fetchKimono() {
        var urlPath = "names have been changed to protect the innocent"
        var url: NSURL = NSURL(string: urlPath)
        var request: NSURLRequest = NSURLRequest(URL: url)
        var connection: NSURLConnection = NSURLConnection(request: request, delegate: self, startImmediately: false)

        connection.start()
    }

    func connection(didReceiveResponse: NSURLConnection!, didReceiveResponse response: NSURLResponse!) {
        // Recieved a new request, clear out the data object
        self.data = NSMutableData()
    }

    func connection(connection: NSURLConnection!, didReceiveData data: NSData!) {
        // Append the recieved chunk of data to our data object
        self.data.appendData(data)
    }

    func connectionDidFinishLoading(connection: NSURLConnection!) {
        // Request complete, self.data should now hold the resulting info
        // Convert the retrieved data in to an object through JSON deserialization
        var err: NSError
        var jsonResult: NSDictionary = NSJSONSerialization.JSONObjectWithData(data, options:    NSJSONReadingOptions.MutableContainers, error: nil) as NSDictionary
        var results: NSDictionary = jsonResult["results"] as NSDictionary
        var collection: NSArray = results["collection1"] as NSArray
        if jsonResult.count>0 && collection.count>0 {
            var results: NSArray = collection as NSArray
            self.tableData = results
            self.tableView.reloadData()
        }
    }
}
MinnuKaAnae
  • 1,646
  • 3
  • 23
  • 35
chandlervdw
  • 3,197
  • 4
  • 19
  • 21
  • So, my first error is that Business actually needs to be an NSDictionary, to match the JSON that's being fetched. That gets rid of all the biz variables. Second error is that I don't need self.tableData, I need Business = jsonResults – but now I get a memory error. Do I need to dealloc or deinitialize? – chandlervdw Jun 09 '14 at 03:31
  • Please post your latest code – Dash Jun 09 '14 at 08:11
  • @Dash added my answer below. – chandlervdw Jun 10 '14 at 15:18

10 Answers10

156

You'll need to reload the table on the UI thread via:

//swift 2.3
dispatch_async(dispatch_get_main_queue(), { () -> Void in
    self.tableView.reloadData()
})

//swift 5
DispatchQueue.main.async{
    self.tableView.reloadData()
}

Follow up: An easier alternative to the connection.start() approach is to instead use NSURLConnection.sendAsynchronousRequest(...)

//NSOperationQueue.mainQueue() is the main thread
NSURLConnection.sendAsynchronousRequest(NSURLRequest(URL: url), queue: NSOperationQueue.mainQueue()) { (response, data, error) -> Void in
    //check error
    var jsonError: NSError?
    let json: AnyObject? = NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions.allZeros, error: &jsonError)
    //check jsonError
    self.collectionView?.reloadData()
}

This doesn't allow you the flexibility of tracking the bytes though, for example you might want to calculate the progress of the download via bytesDownloaded/bytesNeeded

noobular
  • 3,257
  • 2
  • 24
  • 19
  • It is calling this piece of code (I am running a `println`) but still not reloading the data? – Haring10 Oct 26 '14 at 13:27
  • 1
    Make sure your delegates are setup properly and that your delegate methods are actually being called. – noobular Oct 28 '14 at 23:43
  • @noobular why is dispatch_async needed I fully don't understand. This answer helped me a lot (2 hours of googling around). – Kyslik Nov 06 '14 at 23:39
  • 1
    @Kyslik Notice that when you invoke connection.start(), the delegate methods are invoked on a background thread because otherwise the main UI thread is blocked and user experience goes down the toilet. The dispatch_async bit tells the system to execute the reload on the main_queue aka the ui thread. – noobular Nov 08 '14 at 10:31
  • 1
    Also, don't forget to make the ViewController the dataSource of the TableView on the storyboard by ctrl+click on the TableView and dragging it to the little yellow square at the top of the view and choosing "dataSource" – SMKS Nov 28 '15 at 23:49
21

You have just to enter:

First a IBOutlet:

@IBOutlet var appsTableView : UITableView

Then in a Action func:

self.appsTableView.reloadData()
user3752603
  • 227
  • 1
  • 3
4

If your connection is in background thread then you should update UI in main thread like this

self.tblMainTable.performSelectorOnMainThread(Selector("reloadData"), withObject: nil, waitUntilDone: true)

As I have mentioned here

Swift 4:

self.tblMainTable.performSelector(onMainThread: #selector(UICollectionView.reloadData), with: nil, waitUntilDone: true)
Arash HF
  • 837
  • 12
  • 10
Anand Suthar
  • 3,678
  • 2
  • 30
  • 52
  • This fixed it for me. Why did self.collectionView?.reloadData() not work for me? I'm using NSURLSession – ChainFuse Mar 11 '16 at 02:24
2

In my case the table was updated correctly, but setNeedDisplay() was not called for the image so I mistakenly thought that the data was not reloaded.

Dominique Lorre
  • 1,168
  • 1
  • 10
  • 19
1

So, the issue was that I was trying to inappropriately use @lazy, which caused my Business variable to essentially be a constant, and thusly uneditable. Also, instead of loading the local json, I'm now loading only the data returned from the API.

import UIKit

class BusinessTableViewController: UITableViewController {

    var data: NSMutableData = NSMutableData()
    var Business: NSMutableArray = NSMutableArray()

    override func viewDidLoad() {
        super.viewDidLoad()

        navigationItem.titleView = UIImageView(image: UIImage(named: "growler"))

        tableView.registerClass(BeerTableViewCell.self, forCellReuseIdentifier: "cell")
        tableView.separatorStyle = .None

        fetchKimono()
    }

    override func numberOfSectionsInTableView(tableView: UITableView!) -> Int {
        return Business.count
    }

    override func tableView(tableView: UITableView?, numberOfRowsInSection section: Int) -> Int {
        if (Business.count > 0) {
            let biz = Business[section] as NSDictionary
            let beers = biz["results"] as NSArray
            return beers.count
        } else {
            return 0;
        }
    }

    override func tableView(tableView: UITableView?, cellForRowAtIndexPath indexPath: NSIndexPath?) -> UITableViewCell? {
        let cell = tableView!.dequeueReusableCellWithIdentifier("cell", forIndexPath: indexPath!) as BeerTableViewCell
        if let path = indexPath {
            let biz = Business[path.section] as NSDictionary
            let beers = biz["results"] as NSArray
            let beer = beers[path.row] as NSDictionary

            cell.titleLabel.text = beer["BeerName"] as String
        } else {
            cell.titleLabel.text = "Loading"
        }

        return cell
    }

    override func tableView(tableView: UITableView!, viewForHeaderInSection section: Int) -> UIView! {
        let view = LocationHeaderView()
        let biz = Business[section] as NSDictionary
        if (Business.count > 0) {
            let count = "\(Business.count)"
            view.titleLabel.text = (biz["name"] as String).uppercaseString
        }
        return view
    }

    override func tableView(tableView: UITableView!, heightForHeaderInSection section: Int) -> CGFloat {
        return 45
    }

    func fetchKimono() {
        var urlPath = "names have been removed to protect the innocent"
        var url: NSURL = NSURL(string: urlPath)
        var request: NSURLRequest = NSURLRequest(URL: url)
        var connection: NSURLConnection = NSURLConnection(request: request, delegate: self, startImmediately: false)

        connection.start()
    }

    func connection(didReceiveResponse: NSURLConnection!, didReceiveResponse response: NSURLResponse!) {
        // Recieved a new request, clear out the data object
        self.data = NSMutableData()
    }

    func connection(connection: NSURLConnection!, didReceiveData data: NSData!) {
        // Append the recieved chunk of data to our data object
        self.data.appendData(data)
    }

    func connectionDidFinishLoading(connection: NSURLConnection!) {
        // Request complete, self.data should now hold the resulting info
        // Convert the retrieved data in to an object through JSON deserialization
        var err: NSError
        var jsonResult: NSDictionary = NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions.MutableContainers, error: nil) as NSDictionary
        var results: NSDictionary = jsonResult["results"] as NSDictionary
        var collection: NSArray = results["collection1"] as NSArray
        if jsonResult.count>0 && collection.count>0 {
            Business = jsonResult
            tableView.reloadData()
        }
    }
}

Swift Docs on @lazy:

You must always declare a lazy property as a variable (with the var keyword), because its initial value may not be retrieved until after instance initialization completes. Constant properties must always have a value before initialization completes, and therefore cannot be declared as lazy.

chandlervdw
  • 3,197
  • 4
  • 19
  • 21
1

Beside the obvious reloadData from UI/Main Thread (whatever Apple calls it), in my case, I had forgotten to also update the SECTIONS info. Therefor it did not detect any new sections!

Radu
  • 2,076
  • 2
  • 20
  • 40
0

All the calls to UI should be asynchronous, anything you change on the UI like updating table or changing text label should be done from main thread. using DispatchQueue.main will add your operation to the queue on the main thread.

Swift 4

DispatchQueue.main.async{
    self.tableView.reloadData()
}
makkhokher
  • 228
  • 2
  • 15
0

You must reload your TableView in main thread only. Otherwise your app will be crashed or will be updated after some time. For every UI update it is recommended to use main thread.

//To update UI only this below code is enough
//If you want to do changes in UI use this
DispatchQueue.main.async(execute: {
    //Update UI
    self.tableView.reloadData()//Your tableView here
})

//Perform some task and update UI immediately.
DispatchQueue.global(qos: .userInitiated).async {  
    // Call your function here
    DispatchQueue.main.async {  
        // Update UI
        self.tableView.reloadData()  
    }
}

//To call or execute function after some time and update UI
DispatchQueue.main.asyncAfter(deadline: .now() + 5.0) {
    //Here call your function
    //If you want to do changes in UI use this
    DispatchQueue.main.async(execute: {
        //Update UI
        self.tableView.reloadData()
    })
}
Naresh
  • 16,698
  • 6
  • 112
  • 113
0

Try it: tableView.reloadSections(IndexSet(integersIn: 0...0), with: .automatic) It helped me

Said
  • 525
  • 5
  • 4
-3

I was also facing the same issue, what I did wrong was that I'd forgot to add

tableView.delegate = self    
tableView.dataSource = self

in the viewDidLoad() {} method. This could be one reason of self.tableView.reloadData() not working.

Arjun
  • 1,477
  • 1
  • 13
  • 23