I am using the below code to fetch the data from the firestore database in swift iOS. But when I scroll the new data loaded is replacing the previously loaded data in the tableview. I am trying to fix this issue but as of now no good.
The outcome required is that adding new documents to the previously list of documents in the tableview
Below is the code I am implementing. If any more information is required please let me know
CODE
fileprivate func observeQuery() {
fetchMoreIngredients = true
//guard let query = query else { return }
var query1 = query
stopObserving()
if posts.isEmpty{
query1 = Firestore.firestore().collection("posts").order(by: "timestamp", descending: true).limit(to: 5)
}
else {
query1 = Firestore.firestore().collection("posts").order(by: "timestamp", descending: true).start(afterDocument: lastDocumentSnapshot).limit(to: 2)
// query = db.collection("rides").order(by: "price").start(afterDocument: lastDocumentSnapshot).limit(to: 4)
print("Next 4 rides loaded")
print("hello")
}
// Display data from Firestore, part one
listener = query1!.addSnapshotListener { [unowned self] (snapshot, error) in
guard let snapshot = snapshot else {
print("Error fetching snapshot results: \(error!)")
return
}
let models = snapshot.documents.map { (document) -> Post in
if let model = Post(dictionary: document.data()) {
return model
} else {
// Don't use fatalError here in a real app.
fatalError("Unable to initialize type \(Post.self) with dictionary \(document.data())")
}
}
self.posts = models
self.documents = snapshot.documents
if self.documents.count > 0 {
self.tableView.backgroundView = nil
} else {
self.tableView.backgroundView = self.backgroundView
}
self.tableView.reloadData()
self.fetchMoreIngredients = false
self.lastDocumentSnapshot = snapshot.documents.last
}
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let off = scrollView.contentOffset.y
let off1 = scrollView.contentSize.height
if off > off1 - scrollView.frame.height * leadingScreensForBatching{
if !fetchMoreIngredients && !reachEnd{
print(fetchMoreIngredients)
// beginBatchFetch()
// query = baseQuery()
observeQuery()
}
}
}
Instead of calling snapshot.documents, call snapshot.documentChanges. This returns a list of document changes (either .added, .modified, or .removed, and allows you to add, remove, or modify them in your local array as needed... Not tested code just an idea what you ca do ...
snapshot.documentChanges.forEach() { diff in
switch diff.type {
case .added:
if let model = Post(dictionary: diff.document.data()){
self.posts.append(model)
}else {
// Don't use fatalError here in a real app.
fatalError("Unable to initialize type \(Post.self) with dictionary \(document.data())")
}
case .removed:
// add remove case
case .modified:
// add modify case
}
}
Related
I have a collection on Firestore and I listen for changes like this:
func createMatchesListener(){
let db = Firestore.firestore()
guard let currentUid = Auth.auth().currentUser?.uid else { return }
matchesListener = db.collection("Matches").document(currentUid).collection("Matches").addSnapshotListener({ snapshot, error in
if let error = error{
print(error.localizedDescription)
return
}
snapshot?.documentChanges.forEach({ change in
if change.type == .added{
// do things
}
})
})
}
I only want to listen for documents that are actually added to that collection.
In fact, the problem is that whenever I invoke this function I receive all the documents of the collection as added documents and then I also receive documents added later.
How can I listen just for actually added later documents, ignoring the ones already present in the collection? Searching online I didn't find any solution to this issue.
EDIT:
This is the way I tried to solve the problem:
func createMatchesListener(){
guard let currentUid = Auth.auth().currentUser?.uid else { return }
getUidsAlreadyMade { uidsAlreadyMade in
matchesListener = db.collection("Matches").document(currentUid).collection("Matches").addSnapshotListener({ snapshot, error in
if let error = error{
print(error.localizedDescription)
return
}
snapshot?.documentChanges.forEach({ change in
if change.type == .added{
let data = change.document.data()
let userId = data["uid"] as? String ?? ""
if uidsAlreadyMade.contains(userId) == false{
//means the uid is newly created in the collection, do stuff accordingly
arrayOfUidsAlreadyMade.append(currentUid)
}
}
if change.type == .removed{
// if the document has been removed, remove also the id from the array of uids
let data = change.document.data()
let currentUid = data["uid"] as? String ?? ""
arrayOfUidsAlreadyMade.removeAll { $0 == currentUid }
}
})
})
}
}
func getUidsAlreadyMade(completion: #escaping ([String]) -> Void){
guard let currentUid = Auth.auth().currentUser?.uid else { return }
db.collection("Matches").document(currentUid).collection("Matches").getDocuments { snapshot, error in
if let error = error{
print(error.localizedDescription)
return
}
arrayOfUidsAlreadyMade.removeAll()
snapshot?.documents.forEach({ doc in
let dict = doc.data()
let userId = dict["uid"] as? String ?? ""
arrayOfUidsAlreadyMade.append(userId)
})
completion(arrayOfUidsAlreadyMade)
}
}
A simple solution is to include a timestamp in your Firestore documents.
Suppose your documents store Tasks, for example
documentId
task: "get dinner"
timestamp: 20211123
and suppose your app doesn't care about past tasks, only new ones.
When the tasks are read, update the timestamp as to when that occurred.
Then each time after that you want to read only 'new data' specify that in your listener, keeping track of when the last read timestamp was:
db.collection("task").whereField("timestamp", isGreaterThan: lastReadTimeStamp).addSnapshotListener...
The above will only read in tasks that occured after the prior timestamp and add a Listener (reading in all of the new tasks so you can populate the UI).
You can store an array with the ID of the documents that you already have stored in the device. That way, all that you need to do before doing things is checking that document's id is not in your array
There's no way of preventing Firestore from returning the initial snapshot of documents when a document listener is added, so just use a boolean to keep track of the initial snapshot and ignore it.
var listenerDidInit = false
func createMatchesListener(){
let db = Firestore.firestore()
guard let currentUid = Auth.auth().currentUser?.uid else { return }
matchesListener = db.collection("Matches").document(currentUid).collection("Matches").addSnapshotListener({ snapshot, error in
if let error = error{
print(error.localizedDescription)
return
}
if listenerDidInit {
snapshot?.documentChanges.forEach({ change in
if change.type == .added{
// do things
}
})
} else {
listenerDidInit = true
}
})
}
private var listener: ListenerRegistration?
self.listener = db.collection("Matches") // matchesListener
listener!.remove()
So I have this function in class Functions :
struct Prices {
var standardPrice: Int!
}
// FUNC PRICING
class Functions {
private var PricingRef: CollectionReference!
var price = Prices()
func getPrice() -> Prices {
PricingRef = Firestore.firestore().collection("ProductXYZ")
PricingRef.getDocuments { (snapshot, error) in
if let err = error {
debugPrint("Error fetching data \(err)")
}
else {
guard let snap = snapshot else { return }
for document in snap.documents {
let data = document.data()
let std = data["standard"] as! String
self.price.standardPrice = Int(std)!
print(self.price.standardPrice!) // This print the intended result
}
}
}
return price
}
}
Then I want to pass the standardPrice value to this class, called PriceList :
class PriceList: UITableViewController {
var price = Prices()
var newStandardPrice = 0
func Price() {
price = Functions().getPrice()
newStandardPrice = price.standardPrice // always error with value nil
}
I always have that error where newStandardPrice is nil.
but the print(self.price.standardPrice!) shows number of result I want.
So as far as I know, the problem here is because it takes time for the firebase firestore to get the data from database.
How do I get the value of standardPrice after its assigned with the new price from firebase database?
Any help will be appreciated
Thankyou
you need to use completion handler because its async function
func getPrice(completion:#escaping (Prices?,Error?)-> Void) {
PricingRef = Firestore.firestore().collection("ProductXYZ")
PricingRef.getDocuments { (snapshot, error) in
if let err = error {
debugPrint("Error fetching data \(err)")
completion(nil,err)
}
else {
guard let snap = snapshot else { return }
for document in snap.documents {
let data = document.data()
let std = data["standard"] as! String
self.price.standardPrice = Int(std)!
print(self.price.standardPrice!) // This print the intended result
completion(self.price.standardPrice,nil)
}
}
}
}
How to use
Functions().getPrice { (price, error) in
if let err = error {
// do something if you get error
} else if let getPrice = price {
// use price
self.price = getPriice
}
I'm trying to implement pagination in my app. when the app loads I want to load first item lets say. then every time he clicks refresh a new item is loaded.
I have implemented this logic in my viewmodel.
First time homeViewModel.load() is called in my SceneDelegate
let homeViewModel = HomeViewModel()
homeViewModel.refresh()
Then every time users wants to get new dates I call homeViewModel.refresh()
The problem is that when the app loads I do not get any results and when I hit refresh I keep getting the second document in the table over and over again.
What am I doing wrong here?
My HomeViewModel:
class HomeViewModel: ObservableObject, LoadProtocol {
var firestoreService: FirestoreService = FirestoreService()
#Published var items: [Item] = []
let db = Firestore.firestore()
var first: Query = Firestore.firestore().collection("items").limit(to: 1)
load() {
self.first.addSnapshotListener { (snapshot, error) in
guard let snapshot = snapshot else {
print("Error retrieving cities: \(error.debugDescription)")
return
}
guard let lastSnapshot = snapshot.documents.last else {
// The collection is empty.
return
}
// Construct a new query starting after this document,
// retrieving the next 25 cities.
let next = self.db.collection("items")
.start(afterDocument: lastSnapshot).limit(to: 1)
self.first = next
// Use the query for pagination.
}
}
func refresh() {
self.firestoreService.fetchCollection(query: self.first) { (result: Result<[Item], Error>) in
switch result {
case .success(let items):
self.items += items
self.addToCategories()
case .failure(let error):
print(error)
}
}
}
}
Here's your refresh function
func refresh() {
self.firestoreService.fetchCollection(query: self.first) { (result: Result<[Item], Error>) in
switch result {
case .success(let items):
self.items += items
self.addToCategories()
case .failure(let error):
print(error)
}
}
}
There's nothing in that function that advances the cursor further so it will read the same data over and over.
If you want to read the next set of data, the curser need to be moved to the last document after each refresh, like this
func refresh() {
self.first.addSnapshotListener { (snapshot, error) in
if let err = error {
print(err.localizedDescription)
return
}
guard let snapshot = snapshot else { return }
guard let lastSnapshot = snapshot.documents.last else { return }
let next = self.db.collection("items").start(afterDocument: lastSnapshot).limit(to: 2)
self.first = next
for doc in snapshot.documents {
print(doc.documentID)
}
}
}
I'm capturing Firestore data as Firebase shows us, but I don't know how to save the query I make.
In conclusion, what I want to do is bring all the documents that have the same value in your pid field, and then show in a table the product fields and start date, each document in a different cell.
collection food
document: 1
pid:john1
product:Ice
startDate:01/01/2010
document: 2
pid:john1
product:Rice
startDate:01/02/2010
I need to show in the table:
Ice was bought on 01/01/2010
Rice was bought on 01/02/2010
I have this code:
func loadFood(){
pid = "john1"
db = Firestore.firestore()
db.collection("food").whereField("pid", isEqualTo: pid)
.addSnapshotListener { querySnapshot, error in
guard let documents = querySnapshot?.documents else {
print("\n--------------------------------------")
print("Error document: \(error!)")
print("--------------------------------------\n")
return
}
let startDate = documents.map { $0["startDate"]! }
let product = documents.map {$0["product"]!}
let message = ("\(product) was bought \(startDate)")
self.dataRecord.insert(message, at: 0)
DispatchQueue.main.async {
self.tvRecord.reloadData()
}
}
}
I'm showing in the table:
[Ice, Rice] was bought on [01/01/2010, 01/02/2010]
You make a couple of mistakes. First, you loop over the documents multiple times, unnecessarily; that's not very efficient. In your case, you should loop over them once and do all of your data prep in each loop iteration. Second, Firestore has a method specifically for extracting data from document fields called get() which is very easy to read and efficient.
func loadFood(){
pid = "john1"
Firestore.firestore().collection("food").whereField("pid", isEqualTo: pid).addSnapshotListener { querySnapshot, error in
guard let documents = querySnapshot?.documents else {
print("\n--------------------------------------")
print("Error document: \(error!)")
print("--------------------------------------\n")
return
}
for doc in documents {
guard let startDate = doc.get("startDate") as? String,
let product = doc.get("product") as? String else {
continue // continue this loop, not "return" which will return control out of the calling function
}
let message = "\(product) was bought \(startDate)"
dataRecord.append(message)
}
DispatchQueue.main.async {
self.tvRecord.reloadData()
}
}
}
As you deal with the 2 arrays as if they are 1 string instead you need
struct Item {
let message,startDate:String
}
Then
var dataRecord = [Item]()
and finally
self.dataRecord = documents.map { Item(message:$0["product"]!,startDate:$0["startDate"]!)}
Trying to use Dan McGrath's suggested Document Agnostic solution to querying Firestore for random documents, along with the Rinse in Repeat suggestion for pulling multiple random documents.
This code occasionally comes up with nil documents (doesn't always return a document). I think my query is off and am looking for guidance/ideas on how to correct he problem - Thanks
func getRandomPlateOne() {
let plateOneRef = db.collection("plates")
plateOneRef.whereField("random", isGreaterThan: randomNumberOne).order(by: "random").limit(to: 1).getDocuments { (snapshot, error) in
if snapshot!.isEmpty {
plateOneRef.whereField("random", isLessThanOrEqualTo: self.randomNumberOne).order(by: "random", descending: true).limit(to: 1)
} else {
guard let documents = snapshot?.documents else {return}
for document in documents {
let data = document.data()
let newPlate = Plate.init(data: data)
self.randomPlateOne = [newPlate]
print(self.randomPlateOne)
}
}
}
}
EDIT -Though I had this figured out, that passing the random number into a variable, and then using that variable in my query would make certain that the same random number was being used whether the query was going greaterThan or lessThanAndEqualTo. Still getting an occasional nil back from Firestore. My query must still be off.
New code:
func getRandomPlateOne() {
let collectionRef = db.collection("plates")
collectionRef.whereField("random", isGreaterThan: randomNumberOne).order(by: "random").limit(to: 1).getDocuments { (snapshot, error) in
if snapshot!.isEmpty {
collectionRef.whereField("random", isLessThanOrEqualTo: self.randomNumberOne).order(by: "random", descending: true).limit(to: 1)
} else {
guard let documents = snapshot?.documents else {return}
for document in documents {
let data = document.data()
let newPlate = Plate.init(data: data)
self.randomPlateOne = [newPlate]
print(self.randomPlateOne)
}
}
}
}
func generateARandomNumber() {
randomNumberOne = UInt64.random(in: 0 ... 9223372036854775807)
}
var randomNumberOne: UInt64 = 0
EDIT - Function has evolved. I am still unable to get the step between checking if first condition returned a document or not, and moving to a sometimes necessary second query. This works, but I am using a fixed UInt64.
var randomNumberOne: UInt64 = 8190941879098207969 (higher than any other in my collection)
func getRandomPlateOne() {
let randomPlateRef = db.collection("plates")
randomPlateRef.whereField("random", isGreaterThan: randomNumberOne).order(by: "random").limit(to: 1).getDocuments { (snap, error) in
if snap!.isEmpty {
randomPlateRef.whereField("random", isLessThanOrEqualTo: self.randomNumberOne).order(by: "random", descending: true).limit(to: 1).getDocuments { (snap, error) in
print("This is the snapshot from the second query. \(snap!) ")
guard let documents = snap?.documents else {return}
for document in documents {
let data = document.data()
let newPlate = Plate.init(data: data)
self.plates.append(newPlate)
print(self.plates)
}
}
}
}
As I said in my above comment, I was using two different random numbers for working up the range of documents, or down the range of documents when necessary.
I created a generateARandomNumber function, that is called in my viewDidLoad function.
func generateARandomNumber() {
randomNumber = UInt64.random(in: 0 ... 9223372036854775807)
}
That number is then passed into a variable, that is used within my getARandomPlate(a Firestore document).
I am now using the same random number, whether searching for a document whose random number isGreaterThan the viewDidLoad generated random number or if I end up querying for a isLessThanOrEqualTo document.
EDIT -
Working code:
let db = Firestore.firestore()
var randomNumberOne: UInt64 = 0
var plates = [Plate]()
func getRandomPlateOne() {
let randomPlateRef = db.collection("plates")
randomPlateRef.whereField("random", isGreaterThan: randomNumberOne).order(by: "random").limit(to: 1).getDocuments { (snap, error) in
guard let documents = snap?.documents else {return}
for document in documents {
let data = document.data()
let newPlate = Plate.init(data: data)
self.plates.append(newPlate)
print(self.plates)
}
if snap!.isEmpty {
randomPlateRef.whereField("random", isLessThanOrEqualTo: self.randomNumberOne).order(by: "random", descending: true).limit(to: 1).getDocuments { (snap, error) in
guard let documents = snap?.documents else {return}
for document in documents {
let data = document.data()
let newPlate = Plate.init(data: data)
self.plates.append(newPlate)
print(self.plates)
}
}
}
}
}
func generateARandomNumber() {
randomNumberOne = UInt64.random(in: 0 ... 9223372036854775807)
}