IOS/Swift rendering table after JSON request - ios

I am new to IOS and swift. I am trying to implement an api get request that returns json and then display it in a table. Below is my current code. When I run simulator I am getting the following error:
fatal error: Cannot index empty buffer
If I remove the hardcoded return 3 in the tableView function and instead use doDatItems.count nothing renders in the table because I guess the array of doDatItems starts empty before the get request is made. It seems like a timing thing? How do I ensure the get request is made before the table loads?
import UIKit
class ViewController: UIViewController, UITableViewDelegate {
var doDatItems:[String] = []
#IBOutlet weak var doDatItem: UITextField!
#IBOutlet weak var yourDoDats: UILabel!
#IBAction func addDoDat(sender: AnyObject) {
doDatItems.append(doDatItem.text)
println(doDatItems)
}
override func viewDidLoad() {
super.viewDidLoad()
let urlPath = "http://localhost:3000/dodats"
let url: NSURL = NSURL(string: urlPath)!
let session = NSURLSession.sharedSession()
let task = session.dataTaskWithURL(url, completionHandler: {data, response, error -> Void in
println("Task completed")
if((error) != nil) {
// If there is an error in the web request, print it to the console
println(error.localizedDescription)
}
var err: NSError?
var jsonResult = NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions.MutableContainers, error: &err) as NSDictionary
if(err != nil) {
// If there is an error parsing JSON, print it to the console
println("JSON Error \(err!.localizedDescription)")
} else {
let dataArray = jsonResult["dodats"] as [String]
for item in dataArray {
self.doDatItems.append(item)
}
// println(self.doDatItems)
}
})
task.resume()
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 3
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell
{
let cell = UITableViewCell(style: UITableViewCellStyle.Default, reuseIdentifier: "Cell")
println(self.doDatItems)
cell.textLabel?.text = self.doDatItems[indexPath.row]
return cell
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}

I found several problems -
You ViewController should conform to UITableViewDatasource (which is missing, not sure how it went that far)
Do not return 3 when self.doDatItems does not have any items. It will cause a crash. As long as the data loads let the table remain empty. return self.doDatItems.count
Once data is loaded and ready to display from self.doDatItems array, just call reloadData() method of your table view.
Before that, you should have a reference (or IBOutlet) of your tableView so that you can call reloadData() from anywhere.

You need to trigger a page refresh once the data has been received and parsed.
Something along the lines of
self.tableView.reloadData()

In this delegate method, which return number of rows,
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int
Don't use static number as 3. Get your Array Count & return the count in here.
After the json response comes, reload the tableView. In objective-c it's done by
[tableView reloadData];

Related

saving API response to coreData, still shows nil when accessing

func getGenreKeys(complition: #escaping (_ genre : GenreListModel?) -> ())
{
let genreUrl = URL(string: "\(baseUrl)\(genreListUrl)\(apiKey)")!
urlSessionManager(url: genreUrl,toUseDataType: GenreListModel.self) { json in
//json will contain genreList Object , which can be used to get keys
switch json
{
case .success(let genreListData) :
complition(genreListData)
CoreData.shared.saveGenreList(json: genreListData)
case .failure(let error) :
print(error)
}
}
}
this above is the api completion code
func saveGenreList(json: GenreListModel){
let context = persistentContainer.viewContext
let genreList = GenreList(context: context)
json.genres?.forEach({ Genres in
genreList.name = Genres.name
do{
try context.save()
}
catch{
print("error in saving")
}
})
}
this is what i did to save data after completion of api fetch.
var coreGenre : GenreList?
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return coreGenre?.name?.count ?? 0
this above code is the part of VC that requires to get the coreGenre.name to give the count but it is nill
but when i try to access from viewController by creating a variable of the core data entity class , it returns nill
What's wrong?
I'll base my answer on code there. In your GitHub account where you shared that code.
First issue:
You have:
var genrelist : GenreListModel? {
didSet{
//after getting data a table needs to reload and ui elements needs to be used in main thread only
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
}
var coreGenre : [GenreList]?
override func viewWillAppear(_ animated: Bool) {
...
ApiManager.shared.getGenreKeys { genre in
self.genrelist = genre
}
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return coreGenre?.count ?? 0
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
...
genrelist?.genres?[indexPath.row].id
...
cell.genreLabel.text = genrelist?.genres?[indexPath.row].name
...
}
First issue:
coreGenre IS NEVER SET! genreList is set, but not coreGenre. So don't expect it to be something else that nil!
You didn't write coreGenre = something
So tableView(_:numberOfRowsInSection:) will always return 0.
Make your choice. Either genreList or coreGenre:
ApiManager.shared.getGenreKeys { genreList in
self.coreGenre = genreList.genres
}
&
var coreGenre: [GenreList] {
didSet { DispatchQueue.main.async { self.tableView.reloadData() } }
}
And remove var genreList
Or, remove coreGenre.
That should fix the UITableView, and that's where your question is misleading: There is no CoreData here. In the completion of getGenreKeys(), you return the value. CoreData saves the value in the method, but that's all.
It's:
API -> Parse Value -> Save in CoreData
-> Completion -> Reload TableView to Display
Now in your CoreData stack, you have a method saveGenreList(json:). Assuming it works, you save it but never retrieve it.
There is no NSFetchRequest in your code. Look how to execute fetch in CoreData.
Just stop thinking about CoreData, to understand the issue:
let retrieveValueFromAPI = "I got it"
let savedValue = retrieveValueFromAPI // (1)
return retrieveValueFromAPI
In (1), you'll have an unused variable warning from Xcode. It's exactly the same issue. Here its a "RAM value" instead of a disk value, but that's the same logic. You don't use what you've saved.
You need to continue working and debug. And debug is not only fixing the issue. There are a few steps:
Reproduce the issue
Find its origin/understand what could be the issue
Fix the issue
Finding its origin, and why, are the steps you were missing, either by misconcept, or not enough step by step research in your code.

Am I implementing the tableviewdatasource correctly to get downloaded data to show?

I am developing a small app to connect to my site, download data via a PHP web service, and display it in a table view. To get started I was following a tutorial over on Medium by Jose Ortiz Costa (Article on Medium).
I tweaked his project and got it running to verify the Web service was working and able to get the data. Once I got that working, I started a new project and tried to pull in some of the code that I needed to do the networking and tried to get it to display in a tableview in the same scene instead of a popup scene like Jose's project.
This is where I am running into some issues, as I'm still rather new to the swift programming language (started a Udemy course and have been picking things up from that) getting it to display in the table view. I can see that the request is still being sent/received, but I cannot get it to appear in the table view (either using my custom XIB or a programmatically created cell). I thought I understood how the code was broken down, and even tried to convert it from a UITableViewController to a UITableviewDataSource via an extension of the Viewcontroller.
At this point, I'm pretty stumped and will continue to inspect the code and tweak what I think might be the root cause. Any pointers on how to fix would be really appreciated!
Main Storyboard Screenshot
Struct for decoding my data / Lead class:
import Foundation
struct Lead: Decodable {
var id: Int
var name: String
var program: String
var stage: String
var lastAction: String
}
class LeadModel {
weak var delegate: Downloadable?
let networkModel = Network()
func downloadLeads(parameters: [String: Any], url: String) {
let request = networkModel.request(parameters: parameters, url: url)
networkModel.response(request: request) { (data) in
let model = try! JSONDecoder().decode([Lead]?.self, from: data) as [Lead]?
self.delegate?.didReceiveData(data: model! as [Lead])
}
}
}
ViewController:
import UIKit
class LeadViewController: UIViewController {
// Buttons
#IBOutlet weak var newButton: UIButton!
#IBOutlet weak var firstContactButton: UIButton!
#IBOutlet weak var secondContactButton: UIButton!
#IBOutlet weak var leadTable: UITableView!
let model = LeadModel()
var models: [Lead]?
override func viewDidLoad() {
super.viewDidLoad()
//Make Buttons rounded
newButton.layer.cornerRadius = 10.0
firstContactButton.layer.cornerRadius = 10.0
secondContactButton.layer.cornerRadius = 10.0
//Delegate
model.delegate = self
}
//Send request to web service based off Buttons Name
#IBAction func findLeads(_ sender: UIButton) {
let new = sender.titleLabel?.text
let param = ["stage": new!]
print ("findLead hit")
model.downloadLeads(parameters: param, url: URLServices.leads)
}
}
extension LeadViewController: UITableViewDataSource {
func numberOfSections(in tableView: UITableView) -> Int {
// #warning Incomplete implementation, return the number of sections
print ("number of sections hit")
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// #warning Incomplete implementation, return the number of rows
guard let _ = self.models else {
return 0
}
print ("tableView 1 hit")
return self.models!.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// Create an object from LeadCell
let cell = tableView.dequeueReusableCell(withIdentifier: "leadID", for: indexPath) as! LeadCell
// Lead selection
cell.leadName.text = self.models![indexPath.row].name
cell.actionName.text = self.models![indexPath.row].lastAction
cell.stageName.text = self.models![indexPath.row].stage
cell.progName.text = self.models![indexPath.row].program
print ("tableView 2 hit")
// Return the configured cell
return cell
}
}
extension LeadViewController: Downloadable {
func didReceiveData(data: Any) {
//Assign the data and refresh the table's data
DispatchQueue.main.async {
self.models = data as? [Lead]
self.leadTable.reloadData()
print ("LeadViewController Downloadable Hit")
}
}
}
EDIT
So with a little searching around (okay...A LOT of searching around), I finally found a piece that said I had to set the class as the datasource.
leadTable.dataSource = self
So that ended up working (well after I added a prototype cell with the identifier used in my code). I have a custom XIB that isn't working right now and that's my next tackle point.
You load the data, but don't use it. First, add the following statement to the end of the viewDidLoad method
model.delegate = self
Then add the following LeadViewController extension
extension LeadViewController: Downloadable {
func dicReceiveData(data: [Lead]) {
DispatchQueue.main.async {
self.models = data
self.tableView.reloadData()
}
}
}
And a couple of suggestions:
It is not a good practice to use the button title as a network request parameter:
let new = sender.titleLabel?.text
let param = ["stage": new!]
It is better to separate UI and logic. You can use the tag attribute for buttons (you can configure it in the storyboard or programmatically) to check what button is tapped.
You also have several unnecessary type casts in the LeadModel class. You can change
let model = try! JSONDecoder().decode([Lead]?.self, from: data) as [Lead]?
self.delegate?.didReceiveData(data: model! as [Lead])
to
do {
let model = try JSONDecoder().decode([Lead].self, from: data)
self.delegate?.didReceiveData(data: model)
}
catch {}

Execution order of closures

Here's my code:
I'm just a beginner in programming with this language and I have a problem with a dynamic collection view
My problem is that the the first print is executed after the second one and I'm wondering why...
class ViewController: UICollectionViewController, UICollectionViewDelegateFlowLayout {
#IBOutlet weak var menuButton: UIBarButtonItem!
let db = Firestore.firestore()
let cellId: String = "cellId"
var news = [[String: Any]]()
override func viewDidLoad(){
super.viewDidLoad()
navigationItem.title = "Novità"
sideMenu()
customizeNavBar()
collectionView?.register(NovitaCellView.self, forCellWithReuseIdentifier: cellId)
}
override func didReceiveMemoryWarning(){
super.didReceiveMemoryWarning()
}
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
db.collection("News").getDocuments(){(querySnapshot, err) in
if let err = err{
print("Errore: \(err)")
}else{
for document in (querySnapshot?.documents)!{
let cell = document.data()
self.news.append(cell)
print ("document number: \(self.news.count)")
}
}
}
print("exe return with value: \(self.news.count)")
return self.news.count
}
edit: I tried setting it into the viewDidLoad func as well as setting it both as a sync queue and an async queue and it either doesn't works.
edit 2: i made this working by adding the closure into a max priority queue and reloading the view after in the main thread but it takes a long time in order to work..
The idea is that this line
db.collection("News").getDocuments(){(querySnapshot, err) in
is asynchronous (runs in another queue other than the main queue ) it's a network request that will run after the below line (which runs in main queue)
You are going to background thread mode when you call getDocuments which have less priority than Main thread.So replace below part of code into viewDidLoad or a method which is called from viewDidLoad
db.collection("News").getDocuments(){(querySnapshot, err) in
if let err = err{
print("Errore: \(err)")
}else{
for document in (querySnapshot?.documents)!{
let cell = document.data()
self.news.append(cell)
print ("document number: \(self.news.count)")
}
DispatchQueue.main.async {
tableview.reloadData()
}
}
}
and when you get your data you go back to main thread and reload your tableview using this code.
DispatchQueue.main.async {
tableView.reloadData()
}
You can learn more about threading from apple documentation and this tutorial also.

How to reload tableview after adding new entry?

I am creating a cloudkit tableview. I load the app and my tableview appears with my entries from cloud kit.
I then use my add method insertNewObject which adds the record to cloud kit but this does not show up in my tableview. It will only show up on my next run of the app.
func insertNewObject(sender: AnyObject) {
let record = CKRecord(recordType: "CloudNote")
record.setObject("New Note", forKey: "Notes")
MyClipManager.SaveMethod(Database!, myRecord:record)
dispatch_async(dispatch_get_main_queue()) {
self.tableView.reloadData()
}
}
This is my add method. I am calling tableview reload as you can see but nothing is happening.
My tableview creation code:
// Tableview stuff --- Done
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1
}
/////// Get number of rows
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return objects.count
}
//// FIll the table
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath)
let object = objects[indexPath.row]
cell.textLabel!.text = object.objectForKey("Notes") as? String
return cell
}
override func tableView(tableView: UITableView, canEditRowAtIndexPath indexPath: NSIndexPath) -> Bool {
// Return false if you do not want the specified item to be editable.
return true
}
As requested: Method that saves to CloudDB
func SaveMethod(publicDatabase: CKDatabase, myRecord: CKRecord ) -> CKRecord {
publicDatabase.saveRecord(myRecord, completionHandler:
({returnRecord, error in
if let err = error {
self.notifyUser("Save Error", message:
err.localizedDescription)
} else {
dispatch_async(dispatch_get_main_queue()) {
self.notifyUser("Success",
message: "Record saved successfully")
}
}
}))
return myRecord
}
My viewdidload method in masterview:
override func viewDidLoad() {
super.viewDidLoad()
// Database loading on runtime
Database = container.privateCloudDatabase
///Build Query
let query = CKQuery(recordType: "CloudNote", predicate: NSPredicate(format: "TRUEPREDICATE"))
///Perform query on DB
Database!.performQuery(query, inZoneWithID: nil) { (records, error) -> Void in
if (error != nil) {
NSLog("Error performing query. \(error.debugDescription)")
return
}
self.objects = records!
dispatch_async(dispatch_get_main_queue()) {
self.tableView.reloadData()
}
}
You should not reload your entire tableView when you insert a single object. Only do that when you know ALL the data has changed.
To do what you want, this is the order:
Insert a new data object into your datasource (self.objects). Make sure you get the index of where it ends up in the array.
Call insertRowAtIndexPath: with the correct indexPath on your tableView. This will make sure your data and tableView are in sync again, and tableView:cellForRowAtIndexPath: is called for at least your new data object (and possible others, as certain cells might now be reused to display other data).
Note that the order is always: update your data first, then update your UI (the only place I know of that his is hairy is when using a UISwitch).

when to reloadData() on UITableView

Problem: When using Alamofire and SwiftyJSON to populate a UITableView, the table view loads but there is a 1 second pause before the data is displayed. I am not sure where I should be calling reloadData() to fix this.
There are various questions about when to call reloadData() using Alamofire and SwiftyJSON to populate a UITableView, but I have yet to find an answer that solves my problem.
A bit of background:
I am using the Google Places Web API to populate a UITableView with Google Place names, addresses and icons. Originally I was only using SwiftyJSON to accomplish this, but it was taking quite some time for the UITableView to load. Here is some of that code:
GooglePlacesRequest.swift:
...
var placesNearbyArray: [GooglePlaceNearby]?
if let url = NSURL(string: urlString) {
if let data = NSData(contentsOfURL: url, options: .allZeros, error: nil) {
let json = JSON(data: data)
placesNearbyArray = parseNearbyJSON(json)
completion(placesNearbyArray)
} else {
completion(placesNearbyArray)
}
} else {
completion(placesNearbyArray)
}
}
func parseNearbyJSON(json: JSON) -> [GooglePlaceNearby] {
var placesNearbyArray = [GooglePlaceNearby]()
for result in json["results"].arrayValue {
let name = result["name"].stringValue
let address = result["vicinity"].stringValue
...
placesNearbyArray.append(place)
}
return placesNearbyArray
}
And the code from viewWillAppear in the UITableView:
override func viewWillAppear(animated: Bool) {
...
placesRequest.getPlacesNearUserLocation(location, completion: { (googlePlaces) -> Void in
if let googlePlaces = googlePlaces {
self.venues = googlePlaces
}
dispatch_async(dispatch_get_main_queue(), { () -> Void in
self.tableView.reloadData()
})
})
}
As I said above, this approach was working, but the UITableView would often take 3 - 4 seconds to load. I then decided to use Alamofire along with SwiftyJSON and (posssibly) a cache.
Here are some snippets of the current code:
GooglePlacesRequest.swift:
...
var placesNearbyArray: [GooglePlaceNearby]?
request(.GET, urlString).responseSwiftyJSON({ (_, _, json, error) in
var innerPlacesArray = [GooglePlaceNearby]()
for result in json["results"].arrayValue {
let name = result["name"].stringValue
let address = result["vicinity"].stringValue
...
innerPlacesArray.append(place)
}
placesNearbyArray = innerPlacesArray
completion(placesNearbyArray)
})
completion(placesNearbyArray)
}
After adding this, I tried to use the same code from viewWillAppear in the UITableView, but this is what happens:
The UITableView loads much faster
There is then a 1 second pause before the table view cells are populated.
I have tried to place the new request function in various places in the UITableView, such as in the viewDidLoad:
override func viewDidLoad() {
super.viewDidLoad()
...
let placesRequest = PlacesRequest()
placesRequest.fetchNearbyPlaces(location, completion: { (googlePlaces) -> Void in
if let googlePlaces = googlePlaces {
self.venues = googlePlaces
}
dispatch_async(dispatch_get_main_queue(), { () -> Void in
self.tableView.reloadData()
})
}
)
And I have also tried to call reloadData() on the main thread in both viewWillLoad and viewWillAppear.
The result is always the same...there is a 1 second pause before the UITableView loads the data.
Before I even implement my cache, I need to figure out how (and where) to properly make my request. Can anyone assist me with this?

Resources