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)
}
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 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"]!)}
I am using the below code to retrieve the messages in a chat application according to the timestamp but it is not retrieving in order of the timestamp, How should I make sure that messages retrieved are in the order of the timestamp.
I am using Firestore database and Swift IOS for this application
below is the code parts
timestamp saved in database
let timestamp = Int(NSDate().timeIntervalSince1970)
Code to retrieve messages
let ref = Firestore.firestore().collection("messages").order(by: "timestamp", descending: true)
ref.addSnapshotListener { (snapshot, error) in
snapshot?.documentChanges.forEach({ (diff) in
let messageId = diff.document.documentID
let messageRef = Firestore.firestore().collection("messages")
.document(messageId)
messageRef.getDocument(completion: { (document, error) in
guard let dictionary = document?.data() as? [String : Any] else { return }
let message = Message(dictionary: dictionary)
print("we fetched this message \(message.text)")
self.messages.append(message)
DispatchQueue.main.async {
self.collectionView.reloadData()
let indexPath = IndexPath(item: self.messages.count - 1, section: 0)
self.collectionView.scrollToItem(at: indexPath, at: .bottom, animated: true)
}
})
})
}
Perhaps an oversight but what's happening here is the code gets the data you want in descending order by timestamp, but then gets that same data again, which will be unordereed because it's being retrieved asynchronously, and adds to the array.
func doubleGettingData() {
let ref = Firestore.firestore()....
Gets data -> ref.addSnapshotListener { (snapshot, error) in
snapshot?.documentChanges.forEach({ (diff) in
Gets data again -> messageRef.getDocument(completion
To add a bit more context, the 'outside' function shown in the question is in fact getting the documents in the correct order. However, getting those same documents again, they are being returned from Firebase in whatever order they complete in because Firebase calls are asynchronous. This can be proven if we remove all the code except for the two calls. Here's an example Firestore Structure
message_0:
timestamp: 2
message_1
timestamp: 0
message_2
timestamp: 1
and when some print statement are added, here's what's happening
outside func gets: message_0 //timestamp 2
outside func gets: message_2 //timestamp 1
outside func gets: message_1 //timestamp 0
inside func returns: message_1 //timestamp 0
inside func returns: message_2 //timestamp 1
inside func returns: message_0 //timestamp 2
I would make a couple of changes...
Here's my Message class and the array to store the messages in
class Message {
var text = ""
var timestamp = ""
convenience init(withSnap: QueryDocumentSnapshot) {
self.init()
self.text = withSnap.get("text") as? String ?? "No message"
self.timestamp = withSnap.get("timestamp") as? String ?? "No Timestamp"
}
}
var messages = [Message]()
and then the code to read the messages, descending by timestamp and store them in the array. Note
The first query snapshot contains added events for all existing
documents that match the query
func readMessages() {
let ref = Firestore.firestore().collection("messages").order(by: "timestamp", descending: true)
ref.addSnapshotListener { querySnapshot, error in
guard let snapshot = querySnapshot else {
print("Error fetching snapshots: \(error!)")
return
}
snapshot.documentChanges.forEach { diff in
if (diff.type == .added) {
let snap = diff.document
let aMessage = Message(withSnap: snap)
self.messages.append(aMessage)
}
if (diff.type == .modified) {
let docId = diff.document.documentID
//update the message with this documentID in the array
}
if (diff.type == .removed) {
let docId = diff.document.documentID
//remove the message with this documentID from the array
}
}
}
}
This code will also watch for changes and deletions in messages and pass that event to your app when they occur.
I've recently changed a lot on my iOS application and now I got stuck.
I'm trying to insert data from Firestore which looks like this:
So, as you can see I've 6 different names in here.
And here is the code to insert into pickerView.
func getPerson()
{
let authentication = Auth.auth().currentUser?.uid
db.collection("users").document(authentication!).collection("person").getDocuments { (QuerySnapshot, err) in
//If error is not equal to nil
if err != nil
{
print("Error getting documents: \(String(describing: err))");
}
//Succeded
else
{
//For-loop
for _ in QuerySnapshot!.documents
{
//Cleaning the array for the new values
self.personArray.removeAll()
let document = QuerySnapshot!.documents
let data = document.data() //HERE IS THE ERROR
data.forEach { (item) in
if let person1Data = data["name"] as? String
{
self.personArray.append(person1Data)
print(self.personArray)
}
}
}
self.pickerView.reloadAllComponents()
}
}
}
I'm getting the error:
Value of type '[QueryDocumentSnapshot]' has no member 'data'
It used to have QuerySnapshot!.documents.first
but it does not work anymore when I've changed the Firestore data.
Edit:
So. the output is now:
["Joche"] ["Joche", "Joche"] ["Putte"] ["Putte", "Putte"] ["Rebecca"]
["Rebecca", "Rebecca"] ["Fredrik"] ["Fredrik", "Fredrik"] ["Anna"]
["Anna", "Anna"] ["Vickan"] ["Vickan", "Vickan"]
which means it adds everything but x3. How to solve this problem?
data is an instance method of a single QueryDocumentSnapshot not an array , You need
self.personArray.removeAll()
for elem in querySnapshot!.documents {
let data = elem.document.data()
data.forEach {
if let person1Data = $0["name"] as? String {
self.personArray.append(person1Data)
print(self.personArray)
}
}
}