infernouk infernouk - 1 month ago 23
Swift Question

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
}

Answer

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
      let swiftyJsonVar = JSON(jsonResponse)
      completion(swiftyJsonVar)
  }
}

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] (swiftJsonVar) in
        self?.arrRes = swiftJsonVar
        print(arrRes)
        if self?.arrRes.count > 0 {
          DispatchQueue.main.async {
            self?.ExerciseTableView.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.