How I can add pagination in swift? - 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

Related

Search for users that haven't been fetch yet

I have a quick question, I build a block of code that fetches all the users in the database for searching purposes. Preparing for thousands of users in the future, I programmed the method in a pagination way fetching a certain amount of users at a time. That is where the problem lies when I search for a user if the user hasn't been retrieve from the database yet through scrolling I can't search there profile. Does anyone have a suggestion on how I can tackle this?
Here is the code I use to fetch the users:
//create a method that will fetch a certain mount of users
func fetchUsers() {
if userCurrentKey == nil {
USER_REF.queryLimited(toLast: 21).observeSingleEvent(of: .value) { (snapshot) in
self.collectionView?.refreshControl?.endRefreshing()
guard let first = snapshot.children.allObjects.first as? DataSnapshot else { return }
guard let allObjects = snapshot.children.allObjects as? [DataSnapshot] else { return }
allObjects.forEach({ (snapshot) in
let uid = snapshot.key
Database.fetchUser(with: uid, completion: { (user) in
self.users.append(user)
self.collectionView?.reloadData()
})
})
self.userCurrentKey = first.key
}
} else {
USER_REF.queryOrderedByKey().queryEnding(atValue: userCurrentKey).queryLimited(toLast: 22).observeSingleEvent(of: .value, with: { (snapshot) in
guard let first = snapshot.children.allObjects.first as? DataSnapshot else { return }
guard let allObjects = snapshot.children.allObjects as? [DataSnapshot] else { return }
allObjects.forEach({ (snapshot) in
let uid = snapshot.key
if uid != self.userCurrentKey {
Database.fetchUser(with: uid, completion: { (user) in
self.users.append(user)
self.collectionView?.reloadData()
})
}
})
self.userCurrentKey = first.key
})
}
}
}
Here is the code I used to paginate the users:
//once the users pass a certain amount of cells paginate to fetch the next set of users
override func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
if users.count > 20 {
if indexPath.item == users.count - 1 {
print("Fetching...")
fetchUsers()
}
}
}
Lastly here is the code I used to filter through the users:
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
if searchText.isEmpty{
//if search text is empty fetch the users but display nothing
inSearchMode = false
filteredUsers = users
self.collectionView?.refreshControl = refreshController
} else {
//if search text is not empty search for the users
inSearchMode = true
self.collectionView?.refreshControl = nil
filteredUsers = self.users.filter { (user) -> Bool in
return user.username.lowercased().contains(searchText.lowercased())
}
}
//reload the table view data to update the displayed user
self.collectionView?.reloadData()
}
Thank you in advance!
The up front issue is you should not be in a situation where you have to load all users to search through them.
If you have a lot of data in the users node, loading that much data can overwhelm the device and secondly it's going to get very laggy for the user as the loaded data is iterated over.
Your best move is to denormalize your data and let the server do the heavy lifting by performing those queries and delivering only the data you need. Way faster and much easier to maintain. You can also add additional nodes to get to the data you want.
Looking at the code (I don't know your structure) it appears your goal is to have a searchfield where the users can type a username and the goal is to query for that username and return it lowercased.
A solution is to update your Firebase structure. Suppose it's like this
users
uid_0
userName: "MyCrazyUserNAME"
if you want to search, lowercased, add another node to your structure that's a lowercased version of the name
users
uid_0
userName: "MyCrazyUserNAME"
lowerCased: "mycrazyusername"
Then, perform a partial string query on the lowerCased node as the user types
func searchFor(thisPartialString: String) {
let userRef = self.ref.child("users")
let startString = thePartialString
let endString = thisPartialString + "\\uf8ff"
let query = ref.queryOrdered(byChild: "lowerCased")
.queryStarting(atValue: startString)
.queryEnding(atValue: endString")
query.observe....
}
The "\uf8ff" is a character at a very high code level in Unicode - because of that it encompasses all of the preceeding characters.

Query two Cloud Firestore collections into 1 object

I am using Flamelink as a headless CMS integrated with Firebase. All of my string fields are working just fine; I am just having a little trouble getting a URL of the media that was uploaded to Firebase Storage.
The collection that I'm getting my string fields from is fl_content
The fields are:
string1
string2
imageUpload
Within Firebase, I can see the data that gets saved from Flamelink:
string1: "Titanium Tee"
string2: "Lower your handicap by 50 with premium Titanium golf tees!"
imageUpload returns an array with a reference to fl_files (a different collection in Firebase)
imageUpload:
0 fl_files/ZqVeXI3vX0rFDuJVDzuR
Under fl_files > ZqVeXI3vX0rFDuJVDzuR, I'm able to see the full filename of the image I upload; the documents in fl_files have a file field. I need to get this filename sent to my object so that I'm able to use the images in my UI.
Here's my progress:
Task:
struct Task{
var string1:String
var string2:String
//var imageUpload:String
var counter:Int
var id: String
var dictionary: [String: Any] {
return [
"string1": string1,
"string2": string2,
//"imageUpload": imageUpload,
"counter": counter
]
}
}
extension Task{
init?(dictionary: [String : Any], id: String) {
guard let string1 = dictionary["string1"] as? String,
let string2 = dictionary["string2"] as? String,
//let imageUpload = dictionary["imageUpload"] as? String,
let counter = dictionary["counter"] as? Int
else { return nil }
self.init(string1:string1, string2: string2, /*imageUpload: imageUpload,*/ counter: counter, id: id)
}
}
VC:
private var documents: [DocumentSnapshot] = []
public var tasks: [Task] = []
private var listener : ListenerRegistration!
fileprivate func baseQuery() -> Query {
return Firestore.firestore().collection("fl_content").limit(to: 50)
}
fileprivate var query: Query? {
didSet {
if let listener = listener {
listener.remove()
}
}
}
override func viewDidLoad() {
super.viewDidLoad()
self.query = baseQuery()
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
self.listener.remove()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
listenerStuff()
}
func listenerStuff(){
self.listener = query?.addSnapshotListener { (documents, error) in
guard let snapshot = documents else {
print("Error fetching documents results: \(error!)")
return
}
let results = snapshot.documents.map { (document) -> Task in
if let task = Task(dictionary: document.data(), id: document.documentID) {
return task
}
else {
fatalError("Unable to initialize type \(Task.self) with dictionary \(document.data())")
}
}
self.tasks = results
self.documents = snapshot.documents
self.databaseTableView.reloadData()
}
}
How do I query fl_files so that I can populate the imageUpload property of Task with the URL of the uploaded image? Do I do another separate query? Or can I access fl_files from baseQuery()?
EDIT
Here's my attempt at getting to fl_files from fl_content. Trying to just simply populate 2 text fields an an image field (in a UITableViewCell) from Firebase. Is property what I need in getdocument?
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "databaseCellID", for: indexPath) as! DatabaseTableViewCell
let item = tasks[indexPath.row]
cell.label1.text = item.string1
cell.label2.text = item.string2
let docRef = Firestore.firestore().collection("fl_content").document(item.id)
print("PARENT \(docRef.parent)")
docRef.getDocument { (document, error) in
if let document = document, document.exists {
let property = document.get("imageUpload")
// TODO - use this property to get to fl_files?
print("Document data: \(property!)")
}
}
}
You will need to perform a separate query. There are no SQL-like join operations in Firestore, and references are not automatically followed.
The simplest way to do this is probably to write a small Cloud Function that will respond to the uploaded files and place the image URL automatically in your desired collection, allowing for easier querying.
We will attach an onCreate listener to the fl_files collection, then write the downloadURL to the corresponding fl_content document when we see that a new file was created. Note that your actual content fields may be different from the examples I have used here (I'm not personally familiar with Flamelink).
/**
* Firebase Cloud Function deployed to your same project.
*/
const functions = require('firebase-functions');
import { firestore } from 'firebase-admin';
exports.newFileUploaded = functions.firestore
.document('fl_files/{newFileID}')
.onCreate((snap, context) => {
const fileID = context.params.newFileID;
const fileData = snap.data();
// get whatever data you want out of fileData..
// (let's assume there is a downloadURL property with the URL of the image)
const downloadURL = fileData.downloadURL;
// write that download URL to the corresponding fl_content document
// (assuming the file ID is the same as the content ID)
const contentRef = firestore().collection('fl_content').doc(fileID);
// update the imageURL property, returning the Promise, so the function
// does not terminate too early
const updateData = { imageUpload: downloadURL };
return contentRef.update(updateData);
});
Now, you are able to perform just 1 query to fl_content and the new file's image URL will be included in the request. The tradeoff we make is we have to perform 1 additional Firestore write to save 50% on all future read requests.
Alternatively, without a Cloud Function, we would have to perform 2 separate queries to get the content from fl_content and fl_files. As Doug mentioned, it is not possible to perform a JOIN-like query using Firestore due to the scalable way that it was designed.

How do I access Firestore field value without knowing the name?

I am uploading data to Firestore like so:
func uploadTrackedSymptomValues(symptom: String, comment: String, time: String, timestamp: Int) {
print("Uploading symptom values.")
let user_id = FirebaseManager.shared.user_id
let docRef = db.collection("users").document(user_id!).collection("symptom_data").document("\(symptom)_data")
let updateData = [String(timestamp) : ["symptom" : symptom, "severity" : severity, "comment" : comment, "timestamp" : String(timestamp)]]
docRef.setData(updateData, merge: true)
docRef.setData(updateData, merge: true) { (err) in
if err != nil {
print(err?.localizedDescription as Any)
self.view.makeToast(err?.localizedDescription as! String)
} else {
print("Symptom Data Uploaded")
self.view.makeToast("\(symptom) logged at \(time). Severity: \(self.severity). Comment: \(comment)", duration: 2.0, position: .center, title: "Success!", image: self.cellImage) { didTap in
if didTap {
print("completion from tap")
} else {
print("completion without tap")
}
}
}
}
}
On Firestore this looks like:
Now I want to use each field under the timestamp, for a UITableViewCell But I am not sure how to access the field under the document, for example 1556998898 under Anxiety_data so I can access:
comment = "comment";
severity = "Mild";
symptom = "Anxiety";
timestamp = 1556998898;
To use in the UITableViewCell, With realtime database I would use childAdded listener. I have tried:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let index = indexPath.row
let cell = tableView.dequeueReusableCell(withIdentifier: "EntryDataCell", for: indexPath) as! EntryDataCell
cell.configureCustomCell()
let user_id = FirebaseManager.shared.user_id
let symptomRef = db.collection("users").document(user_id!).collection("symptom_data").document(symptomSections[index])
symptomRef.getDocument(completion: { (document, err) in
if let document = document, document.exists {
let dataDescription = document.data().map(String.init(describing:)) ?? "nil"
print("\(document.documentID) : \(dataDescription)")
cell.commentLabel.text = dataDescription //<- Stuck here
} else {
print("Document does not exist")
}
})
return cell
}
But I am stuck as to how I would get the specific field, as I don't know the name as it is the timestamp of when it was made, rather than the whole document. If there is a better way to handle this please let me know. Thank you.
EDIT:
I am able to get the Key (Timestamp) and Values but am having trouble parsing the values:
let obj = dataDescription
for (key, value) in obj! {
print("Property: \"\(key as String)\"") //<- prints Timestamp
print("Value: \"\(value)\"") // <- prints the comment,severity,symptom,timestamp fields I need.
}
If I try to do cell.titleLabel.text = value["comment"] I get Value of type 'Any' has no subscripts
I thought about using a struct:
struct FieldValues: Codable {
let comment: String
let severity: String
let symptom: String
let timestamp: Int
}
But am unsure on how to use it with the value. I feel I am in a bit over my head for this last part.

Display firebase document field to iOS tableview

How can one display the data from the Firestore Collections to tableView on Swift?
This code runs (see below) but I want to import directly from the Firestore database instead of hardtyping data:
var habits = [Habit(id: "1", author: "Maiuran", text: "heyhey")]
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "habitCell", for: indexPath) as! HabitTableViewCell
cell.set(habit: habits[indexPath.row])
return cell
}
The code below works to print to the console but not sure how to display the
let name = Auth.auth().currentUser?.uid
Firestore.firestore().collection("habits").whereField("author", isEqualTo: name).getDocuments() { (querySnapshot, err) in
if let err = err {
print("Error getting documents: \(err)")
} else {
for document in querySnapshot!.documents {
print("\(document.documentID) => \(document.data())")
}
}
}
This is the database: Firestore Database structure
Thank you so much for your time and advice!
You have to check user: if user == nil you must login user before.
override func viewDidLoad() {
super.viewDidLoad()
let user = Auth.auth().currentUser
if user == nil {
login()
} else {
// processing
}
}
Here is a partial solution (now it posts to a given UITextfield so just need to figure out how to post to tableView):
Firestore.firestore().collection("habits").getDocuments(completion: { (snapshot, error) in
snapshot!.documents.forEach({ (document) in
let nametyname = document.data()["author"]
let street = document.data()["friend"]
self.myTextiFieldi.text = nametyname as! String
print(nametyname)
print(street)
})
})

Swift tableview load more data

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.