Swift tableview load more data - swift

I am implementing a method to load more results returned from my sql server when the table view reaches the bottom.
The issue I am facing is that the getData() method is called in a loop and I cant figure out why.
The log repeats the output below until the application is terminated:
Index Path:12
Last Element: 12
Index Path:12
Last Element: 12
I suspect its one of those which is creating the loop but I can't work out what to change.
Any assistance with this is much appreciated
This is my willDisplayCell method:
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
let lastElement = id.count - 1
if indexPath.row == lastElement {
print("Index Path:\(indexPath.row)")
print("Last Element: \(lastElement)")
limitf = "\(lastElement)"
getData()
}
}
and the function I am using to get the data:
func getData(){
let defaults = UserDefaults.standard
let userID = defaults.string(forKey: "id")
if(limitf == ""){
id.removeAll()
firstname.removeAll()
lastname.removeAll()
fbid.removeAll()
image.removeAll()
totalratings.removeAll()
mobile.removeAll()
}
let url = NSURL(string: "https://www.****/getapplications.php?&jobid=\(jobid)&limitf=\(limitf)")
// print(url!)
let task = URLSession.shared.dataTask(with: url as! URL) { (data, response, error) -> Void in
if let urlContent = data {
do {
if let jsonResult = try JSONSerialization.jsonObject(with: urlContent, options: []) as? [[String:AnyObject]] {
var i = 0
while i < jsonResult.count {
self.id.append(jsonResult[i]["id"]! as! String)
self.firstname.append(jsonResult[i]["firstname"]! as! String)
self.lastname.append(jsonResult[i]["lastname"]! as! String)
self.fbid.append(jsonResult[i]["fbid"]! as! String)
self.image.append(jsonResult[i]["image"]! as! String)
self.totalratings.append(jsonResult[i]["totalratings"]! as! String)
self.mobile.append(jsonResult[i]["mobile"]! as! String)
i = i + 1
}
}
} catch {
print("JSON serialization failed")
}
} else {
print("ERROR FOUND HERE")
}
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
task.resume()
}
}

If you only have 12 records on the server, and the 12th record is visible on screen, then every time it is displayed, getData() will be called. The server will probably return no more records (so the count will remain 12) and the call to .reloadData() will cause the 12 records to be displayed again, which will call getData() again on the 12th, and so on ...
You should not call reloadData() when no new records were received.

Your problem is you load self.tableView.reloadData() in getData().
This method means it will run those methods in tableView delegate once again.
As long as you put self.tableView.reloadData() away, this loop will end.

You are calling getData() in willDisplayCell for the last cell being displayed and then clear the contents and reload the table. This means the last cell is always called to reload the data - which is why it keeps looping.
What you should do is detect as you are doing when the last row is loaded, but when you call getData(), rather than clearing out the existing data, you should append the new data rows and use insertRowsAtIndexPaths to update your tableview.

Related

How I can add pagination in swift?

I have spend so much time to find a solution with the documentation from Firebase without any success. I using Swift 5.3 and Firestore and have the following code:
func readFlights() {
Spinner.startAnimating()
let myquery = db.collection("flight").limit(to: 25).whereField("Userid", isEqualTo: userID).order(by: "DateDB", descending: true)
.order(by: "Start", descending: true)
myquery.getDocuments() { (querySnapshot, err) in
if let err = err {
print("Error getting documents: \(err)")
} else {
for document in querySnapshot!.documents {
self.prefixArray.append(document.get("Prefix") as! String)
self.dateArray.append(document.get("Date") as! String)
self.startArray.append(document.get("Start") as! String)
self.stopArray.append(document.get("Stop") as! String)
self.landingArray.append(document.get("Landing") as! String)
self.takeOffArray.append(document.get("Takeoff") as! String)
self.flightTimeArray.append(document.get("FlightTime") as! String)
self.engineTimeArray.append(document.get("EngineTime") as! String)
self.idArray.append(document.get("id") as! String)
self.destinationArray.append(document.get("Destination") as! String)
self.originArray.append(document.get("Origin") as! String)
self.informationArray.append(document.get("Addinfo") as! String)
self.rulesArray.append(document.get("VFRIFR") as! Int)
self.pilotCopilotArray.append(document.get("PilotoCopiloto") as! Int)
self.engineArray.append(document.get("MnteMlte") as! Int)
self.dayNightArray.append(document.get("DayNight") as! Int)
}
DispatchQueue.main.async{
self.tabelView.reloadData()
self.Spinner.stopAnimating()
}
}
}
working fine but I need to include in this code pagination. That means when I received the first 25 records from Firestore and I slip down in the list with my finger so I want after the latest record he load 25 records more and show them.
I would appreciate your help. Thank you
First, create a document cursor that is an instance property of the view/view controller:
var cursor: DocumentSnapshot?
let pageSize = 25 // for convenience
Second, apply the page size to the query:
let myquery = db.collection("flight").limit(to: pageSize).whereField("Userid", isEqualTo: userID).order(by: "DateDB", descending: true).order(by: "Start", descending: true)
Third, whenever you receive a snapshot from Firestore, update the cursor at some point in the return (ideally, after you've unwrapped the snapshot and before you've parsed the documents):
func getData() {
myquery.getDocuments(completion: { (snapshot, error) in
...
if snapshot.count < pageSize {
/* this return had less than 25 documents, therefore
there are no more possible documents to fetch and
thus there is no cursor */
self.cursor = nil
} else {
/* this return had at least 25 documents, therefore
there may be more documents to fetch which makes
the last document in this snapshot the cursor */
self.cursor = snapshot.documents.last
}
...
})
}
Finally, whenever the user scrolls to the bottom, fetch another page using the cursor:
func continueData() {
guard let cursor = cursor else {
return // no cursor, exit
}
myquery.start(afterDocument: cursor).getDocuments(completion: { (snapshot, error) in
...
// always update the cursor whenever Firestore returns
if snapshot.count < self.pageSize {
self.cursor = nil
} else {
self.cursor = snapshot.documents.last
}
...
})
}
For a fluid user experience, you will need to greatly refine this code, but this is the foundation from which you can paginate Firestore. You can also paginate in Firestore using a document offset (instead of a document cursor) but this is to be avoided (refer to documentation for the reasons).
You can use awesome solution from: https://github.com/pronebird/UIScrollView-InfiniteScroll
For your example:
tableView.addInfiniteScroll { (tableView) -> Void in
readFlights("offset if need")
tableView.finishInfiniteScroll()
}
By using UITableViewDelegate, u can call the function. Each time when you scroll to the bottom, it will check the max of your limit and if the condition is true, then fetch data again.
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
let lastItem = self.array.count - 1
if indexPath.row == lastItem {
if limit < max_limit {
limit += 25
//Get data from Server
readFlights(limit:Int)
}
}
}
The max_limit means the total amount of limits, usually, it returned by server in meta

How would I parse all of the data from a paged API at once in Swift 4?

I'm making an app with a tableview and search controller using the rick and morty API, https://rickandmortyapi.com/api/character/. The API is paged and I'm able to parse the data from the first page and display it to my tableview. I'm also able get the other pages of the API when I scroll through the tableview. I can't seem to figure out how to parse all the data from the pages at once. When I use the search controller I can't search for all the characters, until I scroll through the tableView to get all of the characters. I want to be able to search for any character without having to scroll through the tableView first. How would I parse all the data from the different pages at once and display the data to the tableview? Any help is appreciated, thank you!
This is my current code for parsing the data and getting the other pages when scrolling through the tableView
func getIntitalRickAndMortyData(){
downloadedDataArray = []
//here first page is next page
nextPageUrl = "https://rickandmortyapi.com/api/character/"
getRickAndMortyData()
filteredCharacterArray = downloadedDataArray
}
func getRickAndMortyData() {
//construct the url, use guard to avoid nonoptional
guard let urlObj = URL(string: nextPageUrl) else
{ return }
//fetch data
URLSession.shared.dataTask(with: urlObj) {[weak self](data, response, error) in
//to avoid non optional in JSONDecoder
guard let data = data else { return }
do {
//decode object
let downloadedRickAndMorty = try JSONDecoder().decode(PagedCharacters.self, from: data)
self?.downloadedDataArray.append(contentsOf: downloadedRickAndMorty.results)
self?.nextPageUrl = downloadedRickAndMorty.info.next
self?.filteredCharacterArray = (self?.downloadedDataArray)!
self?.currentPage += 1
DispatchQueue.main.async {
self?.tableView.reloadData()
}
//print(self?.aryDownloadedData as Any)
} catch {
print(error)
}
}.resume()
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let count = self.downloadedDataArray.count
if count > 1 {
let lastElement = count - 1
if indexPath.row == lastElement {
//call get api for next page
getRickAndMortyData()
}
}
guard let cell = tableView.dequeueReusableCell(withIdentifier: "rickandmortyCell") as? CharacterTableViewCell else { return UITableViewCell() }
let results: Results
if isFiltering() {
results = filteredCharacterArray[indexPath.row]
} else {
results = downloadedDataArray[indexPath.row]
}
cell.selectionStyle = .none
cell.nameLabel.text = results.name
cell.statusLabel.text = results.status
cell.genderLabel.text = results.gender
cell.originLabel.text = results.origin.name
cell.lastlocationLabel.text = results.location.name
let id = String(results.id)
cell.idLabel.text = id
return cell
}
}
I've tried doing it using a while loop and keeping a current page count and incrementing it, but nothing displays and I get this in my console "XPC connection interrupted"
func getAllRickAndMortyData() {
while currentPage <= 25 {
getRickAndMortyData()
}
}

load large data from firestore to table view Swift

firestore to store about more than 500 information and I want to display it to table view. Basically, I have successfully display all the data in my cell, but the problem is, it takes more than 1 minute to load all data. While the data loaded, I cannot scroll the table view, unless all data finish load. How to enable scrolling while the data is still loading? If not possible, how to load first 20 data first, and will continue load if user is at the end of the cell? Here is some code that I have tried to
get data from firestore:
func getData () {
db.collection("fund").getDocuments()
{
(querySnapshot, err) in
if let err = err
{
print("Error getting documents: \(err)");
}
else
{
for document in querySnapshot!.documents {
let data = document.data()
let agencyPath = data["agensi"] as? String ?? ""
let title = data["title"] as? String ?? ""
let program = data["program"] as? String ?? ""
let perniagaan = data["perniagaan"] as? String ?? ""
let newMax = data["max"] as? Int
let agencyId = document.documentID
let query = Firestore.firestore().collection("Agensi")
let newQuery = query.whereField("name", isEqualTo: "\(agencyPath)")
newQuery.getDocuments()
{
(querySnapshot, err) in
if let err = err {
print("Error getting documents: \(err)");
} else
{
for document in querySnapshot!.documents {
let data = document.data()
let logo = data["logo"] as? String ?? ""
//store to Struct
let newModel = DisplayModel(agency: title, agencyId: agencyId, programTag: program, perniagaanTag: perniagaan, max: newMax, agencyPath: agencyPath, logoUrl: logo, agencyTitle: agencyPath)
self.agencyList.append(newModel)
}
self.tableView.reloadData()
self.dismiss(animated: false, completion: nil)
}
}
}
}
}
}
display data on cell:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cellData: DisplayModel
if searchController.searchBar.text != "" {
cellData = filteredData[indexPath.row]
} else {
cellData = agencyList[indexPath.row]
}
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as? HomeTableViewCell
cell?.agencyName.text = cellData.agency
cell?.agencyImage.sd_setImage(with: URL(string: "\(cellData.logoUrl ?? "")"), placeholderImage: UIImage(named: "no_pic_image"))
return cell!
}
Action on last row of cell:
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
if searchController.searchBar.text != "" {
let lastElement = filteredData.count - 1
if indexPath.row == lastElement {
//getData()
// handle your logic here to get more items, add it to dataSource and reload tableview
}
} else {
let lastElement = agencyList.count - 1
if indexPath.row == lastElement {
//getData()
// handle your logic here to get more items, add it to dataSource and reload tableview
}
}
}
I really have no idea what method I should do to load 20 data first and continue load at the end of cell row, if there is no solution, at least I could scroll the table view during the load session. Thank You, for your information, i just learn swift last month. Thank you for helping me.
You should definitly adopt the UITableViewDataSourcePrefetching protocol.
Check some blogs, like:
https://www.raywenderlich.com/187041/uitableview-infinite-scrolling-tutorial
and adopt it to pagination as described here:
https://firebase.google.com/docs/firestore/query-data/query-cursors

Remote Data won't show on tableView

I'm clueless as to what is wrong. My console doesn't give me any errors, my code seems fine but nothing is showing up. Could someone check my code, see why it doesn't want to work? My tableView is connected with its delegates and source. Not sure what is the problem.
Here is my code:
private let cellIdentifier = "cell"
private let apiURL = "api link"
class TableView: UITableViewController {
//TableView Outlet
#IBOutlet weak var LegTableView: UITableView!
//API Array
var legislatorArray = [congressClass]()
func getLegislators (fromSession session: NSURLSession) {
//Calling url
if let jsonData = NSURL(string: apiURL) {
// Requesting url
let task = session.dataTaskWithURL(jsonData) {(data, response, error) -> Void in
//Check for errors
if let error = error {print(error)
} else {
if let http = response as? NSHTTPURLResponse {
if http.statusCode == 200 {
//Getting data
if let data = data {
do {
let legislatorData = try NSJSONSerialization.JSONObjectWithData(data, options: .MutableContainers)
//Get API data
if let getData = legislatorData as? [NSObject:AnyObject],
findObject = getData["results"] as? [AnyObject]{
//Return data
for cellFound in findObject{
if let nextCell = cellFound["results"] as? [NSObject:AnyObject],
name = nextCell["first_name"] as? String,
lastName = nextCell["last_name"] as? String,
title = nextCell["title"] as? String,
partyRep = nextCell["party"] as? String,
position = nextCell ["position"] as? String,
id = nextCell ["bioguide_id"] as? String
{
//Add data to array
let addData = congressClass(name: name, lastName: lastName, title: title, party: partyRep, position: position, bioID: id)
self.legislatorArray.append(addData)
}
}//end cellFound
//Adding data to table
dispatch_async(dispatch_get_main_queue()) { () -> Void in
self.tableView.reloadData()
}
}
}
//end do
catch {print(error)}
}//end data
}//end statusCode
}//end http
}//else
}//end task
//Run code
task.resume()
}//end jsonData
}
override func viewDidLoad() {
super.viewDidLoad()
let sessionConfig = NSURLSessionConfiguration.defaultSessionConfiguration()
let urlSession = NSURLSession(configuration: sessionConfig)
getLegislators(fromSession: urlSession)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
// MARK: - Table view data source
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
// #warning Incomplete implementation, return the number of sections
return 1
}
//TableView Rows
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return legislatorArray.count
//return 5
}
//Cell Configuration
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier(cellIdentifier, forIndexPath: indexPath) as! CellTableView
cell.lesName?.text = legislatorArray[indexPath.row].name + " " + legislatorArray[indexPath.row].lastName
cell.lesTitle?.text = legislatorArray[indexPath.row].title
cell.lesParty?.text = legislatorArray[indexPath.row].party
//These tests worked fine.. the tableView is working. But the data doesn't seem to pass.
//cell.lesName.text = "Name" + " " + "lastName"
//cell.lesTitle.text = "Title goes here"
//cell.lesParty.text = "D"
return cell
}
}
You're not reloading the tableView
The problem is in this piece of code
//-----------------------------
//New empty array for api data
var indexPath:[NSIndexPath] = []
//Adding data to new array
for i in 0..<self.legislatorArray.count{
let secondIndexPath = NSIndexPath(forRow: i, inSection: 0)
indexPath.append(secondIndexPath)
}
//Adding data to table
dispatch_async(dispatch_get_main_queue()) { () -> Void in
self.tableView.insertRowsAtIndexPaths(indexPath, withRowAnimation: .Left)
}
You don't need any of that. You can just reload the tableView as follows:
//Adding data to table
dispatch_async(dispatch_get_main_queue()) { () -> Void in
//You only need to reload it and that should do the trick
self.tableView.reloadData()
}
I know you said your tableView is connected to the delegate and dataSource but it's not showing in your code.
You conformed the ViewController to the correct protocols but you need something like this in your viewDidLoad.
self.tableView.deletage = self
self.tableView.dataSource = self
//I don't know if this was a typo but in your cellForRowAtIndexPath you are using CellTableView
let nibName = UINib(nibName: "CellTableView", bundle:nil)
self.tableView.registerNib(nibName, forCellReuseIdentifier: cellIdentifier)
I created an example of a better design for your implementation
This is for the WebService and your Custom Class
https://github.com/phantomon/Stackoverflow/blob/master/SO1/MyTableView/MyTableView/Models/WebServiceManager.swift
This is for the ViewController with your tableView
https://github.com/phantomon/Stackoverflow/blob/master/SO1/MyTableView/MyTableView/ViewController.swift
You just need to modify the UITableViewCell with your custom one.
And of course review your custom class data.

Facebook Friends List in TableView: Think need Async Programming?

I am trying to get a friend list from Facebook, i have the call request inside the tableview cellforrowatindexpath but i also have it in a separate function too. Thought to try both ways but i get the same result.
If i do a print from inside the completion block i do get results, however, if i try to print friendProfile outside the completion block it returns nil and therefore when i create the table cells they unwrap nil and the code fails. I think it is because the completion block is not returning data until after i call to create the cells. I am reading, and perhaps this has to do with Asynchronous programming? Or needing to wait till data is returned from Facebook? Or will the completion handler always return nil?
`
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var request = FBSDKGraphRequest(graphPath:"/me/taggable_friends", parameters: ["fields":"user_id,first_name,last_name,name,picture.type(large)"]);
request.startWithCompletionHandler ({ (connection : FBSDKGraphRequestConnection!, result : AnyObject!, error : NSError!) -> Void in
if error == nil {
//print("Friends are : \(result)")
let jsonResult = result
self.friendsArray = jsonResult["data"] as! NSArray
var index = 0
for data in self.friendsArray {
let fn = data["first_name"]! as! String
let ln = data["last_name"]! as! String
let id = data["id"]! as! String
let picture = data["picture"] as! NSDictionary
let parsePic = picture["data"] as! NSDictionary
let url = parsePic["url"] as! String
self.friendProfile .updateValue((fn + " " + ln), forKey: id)
self.friendProfilePic .updateValue(url, forKey: id)
self.friendProfileID . updateValue(id, forKey: index)
index++
}
} else {
print("Error Getting Friends \(error)");
}
//if i do a print(friendprofilId) in here it works
})
//but a print(friendprofilId) here returns nil
let cellidentifier = "SettingsTableViewCell"
let cell = tableView.dequeueReusableCellWithIdentifier(cellidentifier, forIndexPath: indexPath) as! SettingsTableViewCell
let id = self.friendProfileID[indexPath.row]
print(self.friendProfileID)
if (id != nil) {
let name = self.friendProfile[id!]
let url = self.friendProfilePic[id!]
// cell.imageView?.image = returnFriendProfileImage(url!)
cell.textLabel!.text = name
}
return cell
}
`
You need to call tableView.reloadData() in the place where you have it commented that it works. Also, don't retrieve your data in cellForRowAtIndexPath. Just call the function you had in viewDidLoad
An asynchronous function call with return immediately, while the completion block will complete once it receives a response from the web server. In your case, you call startWithCompletionHandler, which returns immediately and configure your cell before the completion block receives a response from Facebook's server. It makes sense that friendProfilId is still null since you haven't received the data from Facebook.