Firestore Querying a Snapshot within a Snapshot? - swift

I am trying to listen for any notifications whenever someone has replied to a post that a user has commented on. Below is how my database structure looks.
Posts: (Collection)
Post 1: (Document)
replies: [user2, user3]
Replies: (Collection)
Reply 1: (Document)
ownerId: [user2]
Reply 2: (Document)
ownerId: [user3]
Currently my code has 2 snapshot listeners. The first one listens to the Posts collections, where a user is inside the 'replies' array. Then the second one listens to the Replies collection, where it returns all documents added that != the current user. When a new reply has been detected, it will set the Tab Bar item's badge.
This currently works right now, but I am curious if there is a better method of doing so.
func getNotifications() {
database.collection("Posts")
.whereField("replies", arrayContains: userData["userId"]!)
.order(by: "timestamp", descending: true)
.limit(to: 70)
.addSnapshotListener() { (querySnapshot, err) in
if let err = err {
print("Error getting documents: \(err)")
}
else {
guard let snapshot = querySnapshot else {
print("Error fetching snapshots: \(err!)")
return
}
snapshot.documentChanges.forEach { documentd in
if (documentd.type == .added) {
let dataTemp = documentd.document.data()
let ifUser = dataTemp["ownerId"] as! String
if(ifUser == self.userData["userId"]!) {
database.collection("Posts")
.document(documentd.document.documentID)
.collection("Replies")
.whereField("timestamp", isGreaterThan: dataTemp["timestamp"] as! Int)
.addSnapshotListener() { (querySnapshot3, err) in
if let err = err {
print("Error getting documents: \(err)")
}
else {
guard let snapshot = querySnapshot3 else {
print("Error fetching snapshots: \(err!)")
return
}
snapshot.documentChanges.forEach { diff in
if (diff.type == .added) {
let temp = diff.document.data()
if((temp["ownerId"] as! String) != self.userData["userId"]!) {
print("new reply")
newArr.append(diff.document.data())
let data = diff.document.data()
let firebaseTime = data["timestamp"] as! Int
let date = lround(Date().timeIntervalSince1970)
if(firebaseTime+10 > date) {
self.tabBar.items![2].badgeValue = "●"
self.tabBar.items![2].badgeColor = .clear
self.tabBar.items![2].setBadgeTextAttributes([NSAttributedString.Key.foregroundColor: UIColor.red], for:.normal)
}
}
}
}
}
}
}
else {
database.collection("Posts")
.document(documentd.document.documentID)
.collection("Replies")
.whereField("ownerId", isEqualTo: self.userData["userId"]!)
.order(by: "timestamp", descending: false)
.limit(to: 1)
.getDocuments() { (querySnapshot2, err) in
if let err = err {
print("Error getting documents: \(err)")
}
else {
var timestamp = Int()
for documentde in querySnapshot2!.documents {
let temp = documentde.data()
timestamp = temp["timestamp"] as! Int
database.collection("Posts")
.document(documentd.document.documentID)
.collection("Replies")
.whereField("timestamp", isGreaterThan: timestamp)
.addSnapshotListener() { (querySnapshot3, err) in
if let err = err {
print("Error getting documents: \(err)")
}
else {
guard let snapshot = querySnapshot3 else {
print("Error fetching snapshots: \(err!)")
return
}
snapshot.documentChanges.forEach { diff in
if (diff.type == .added) {
let temp = diff.document.data()
if((temp["ownerId"] as! String) != self.userData["userId"]!) {
print("new reply")
newArr.append(diff.document.data())
let data = diff.document.data()
let firebaseTime = data["timestamp"] as! Int
let date = lround(Date().timeIntervalSince1970)
if(firebaseTime+10 > date) {
self.tabBar.items![2].badgeValue = "●"
self.tabBar.items![2].badgeColor = .clear
self.tabBar.items![2].setBadgeTextAttributes([NSAttributedString.Key.foregroundColor: UIColor.red], for:.normal)
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}

Your code does not have only two listeners, but one listener per post for which the user you are interested in has ever replied for. This will lead to terrible performances very soon and will potentially crash you app as Firestore has a limitation of 100 listeners per client.
I would advise to redesign your data model:
Only one listener on posts that the user has ever replied to (your first listener)
On each reply increment a reply counter in the post doc, this will trigger the snapshot above.
Optimisation 1: on each action on a post you could set a field lastactiontype, which would have a specific value reply for replies only. This way the snapshot is only triggered on replies.
Optimisation 2: set a field timestamp on each action to the current time and only pull the last n (for instance 10) posts in your snapshots, this will limit the number of reads at load time. You will have to implement some special logic to handle the case when your app goes offline and back online and all n posts of the snapshot have changed. This is a must if your app is meant to scale (you dont want a snapshot with no limit on a collection with 100k docs...)
Example:
firestore.collection("Posts")
.where( "lastaction", "==" , "reply")
.where( "replies", "array-contains", uid)
.orderBy("timestamp", "desc").limit(10)

Related

SiwftUI Firebase modifying variable inside snapshot change not working

The following is a function to add a listener to a query. Whenever a document is added/removed I make some changes on two arrays (one of the user Ids and one of the user details). As you can see I tried printing everything: I correctly receive the data whenever it is added/removed, I can retrieve the document ID I need but whenever I append it to the usersReqestedUIDs array it always prints it as empty, even if I try to append a random string in it. Why is that?
func addRequestedUsersSnapshot() {
let db = Firestore.firestore()
let userRef = db.collection("user").document(user.UID)
let userRequestedRef = userRef.collection("friends").whereField("status", isEqualTo: "request")
// First query to fetch all friendIDs
userRequestedRef.addSnapshotListener { querySnapshot, error in
guard let snapshot = querySnapshot else {
print("Error fetching snapshots: (error!)")
return
}
snapshot.documentChanges.forEach { diff in
print("ids : (self.usersReqestedUIDs)")
print("type : (diff.type)")
if diff.type == .added {
print("doc id (diff.document.documentID)")
self.usersReqestedUIDs.append("hello")
print("added (diff.document.data())")
print("ids : (self.usersReqestedUIDs)")
self.fetchUserDetailsByUID(uid: diff.document.documentID) { result in
switch result {
case let .success(user):
self.usersReqestedDetails.append(user)
case let .failure(error):
print(error)
}
}
}
if diff.type == .removed {
print("removed (diff.document.data())")
self.usersReqestedDetails.removeAll(where: { $0.UID == diff.document.documentID })
self.usersReqestedUIDs.removeAll(where: { $0 == diff.document.documentID })
}
if diff.type == .modified {
print("modified (diff.document.data())")
}
}
}
}

Do two Firestore listeners that overlap bill for the same document update?

I have two listeners both observing the same collection 'Tasks'.
One query retrieves tasks that are incomplete (completed == false)
The other query retrieves complete tasks within the last 24 hours (completed == true && completionDate > (Date - 24 hours))
Due to this logic the two listeners happen to overlap each other. If I update a task to complete. Both snapshot listeners will run. This is the same document however.
Q: Would I get charged TWICE for just updating one document? or will the listeners know that the same document has been updated once and will only be billed once?
class TaskData: ObservableObject {
#Published var tasks: [Task] = []
#Published var incompleteTasks: [Task] = []
#Published var completedTasks: [Task] = []
private var db = Firestore.firestore()
private var incompleteTaskslistenerRegistration: ListenerRegistration?
private var completedTasksListenerRegistration: ListenerRegistration?
init() {
subscribe()
Publishers
.CombineLatest($completedTasks, $incompleteTasks)
.dropFirst()
.map { $0 + $1 }
.assign(to: &$tasks)
}
deinit {
unsubscribe()
}
func subscribe() {
guard let userId = Auth.auth().currentUser?.uid else { return }
if incompleteTaskslistenerRegistration != nil || completedTasksListenerRegistration != nil {
unsubscribe()
}
let completedTasksQuery = db.collection("tasks")
.whereField("userId", isEqualTo: userId)
.whereField("completed", isEqualTo: true)
.whereField("completedDate", isGreaterThan: Date().advanced(by: -86400))
completedTasksListenerRegistration = completedTasksQuery.addSnapshotListener { querySnapshot, error in
guard let documents = querySnapshot?.documents else {
print("No documents in 'tasks' collection")
return
}
self.completedTasks = documents.compactMap { queryDocumentSnapshot in
try? queryDocumentSnapshot.data(as: Task.self)
}
}
let incompleteTasksQuery = db.collection("tasks")
.whereField("userId", isEqualTo: userId)
.whereField("completed", isEqualTo: false)
incompleteTaskslistenerRegistration = incompleteTasksQuery.addSnapshotListener { querySnapshot, error in
guard let documents = querySnapshot?.documents else {
print("No documents in 'tasks' collection")
return
}
self.incompleteTasks = documents.compactMap { queryDocumentSnapshot in
try? queryDocumentSnapshot.data(as: Task.self)
}
}
}
func unsubscribe() {
if incompleteTaskslistenerRegistration != nil {
incompleteTaskslistenerRegistration?.remove()
incompleteTaskslistenerRegistration = nil
}
if completedTasksListenerRegistration != nil {
completedTasksListenerRegistration?.remove()
completedTasksListenerRegistration = nil
}
}
}
Edit 1
I just want to clarify that the reason I have the query for retrieving completed tasks for the past 24 hours is so that users are able to uncheck or make changes in that period. It will then be no longer fetched. I don’t want add a listener that fetches all the tasks ever made as that would start costing a lot and I don’t want to implement pagination as I lose that real-time updating and pagination for listeners is rather strenuous.
Cloud Firestore charges you for the number of documents you read, write and delete, so you are charged for a read each time a document in the result is updated.
Since you have two active listeners that run the same document, the first listener will need to get or check the data from the server (document) while the second listener can read it from cache.

firestore wherefield result is <FIRQuery> in swift

my code is here and result
func documentField(){
guard let uid = Auth.auth().currentUser?.uid else {
return
}
print(uid)
let db = Firestore.firestore().collection("collection")
let data = db.orderby(by: "users").whereField("users", arraycontains: uid)
print(data)
}
console outline
MNDJR2NOx1gOcxPGJ2xOUw3PHCM2
<FIRQuery: 0x6000032555e0>
i dont know where is my fault this query result is every time comig <FIRQuery: 0x6000032555e0>
Your code creates a query, but doesn't execute it. So that means you're printing the query itself, not its results.
If you have a look at the documentation on getting documents from the database, you'll find this Swift example of how to do so:
db.collection("cities").whereField("capital", isEqualTo: true)
.getDocuments() { (querySnapshot, err) in
if let err = err {
print("Error getting documents: \(err)")
} else {
for document in querySnapshot!.documents {
print("\(document.documentID) => \(document.data())")
}
}
}
You're going to want to call getDocuments() on your query in the same way and process the results you get.

Handling documentID in FirebaseFirestoreSwift is really confusing

I have to query a slew of collections and the models are always defined something like this:
struct Order : Identifiable, Codable {
#DocumentID var id : String?
let fieldOne : String?
let fieldTwo : Int?
enum CodingKeys : String, CodingKey {
case id // (had to comment this out)
case fieldOne
case fieldTwo
}
}
Today, I spent all day trying to figure out why I couldn't load documents from for one particular collection. I was getting a snapshot with documents but could not convert and populate them into an array. After hours of trial and error I commented out the "case id" in the enum and got it to work.
Any idea why this is happening?
Here's a query which works WITH the case id:
listener = db.collection("Meal_Plans").whereField("userId", isEqualTo: userEmail).order(by: "timeOfCreation", descending: true).addSnapshotListener({ (querySnapshot, error) in
if let error = error {
print("error in mp query: \(error.localizedDescription)")
} else {
guard let documents = querySnapshot?.documents else {
print("No mealplans")
return
}
let mealplanArray: [Mealplan] = documents.compactMap { queryDocumentSnapshot -> Mealplan? in
return try? queryDocumentSnapshot.data(as: Mealplan.self)
}
let planViewModel = mealplanArray.map({return PlanViewModel(mealplan: $0)})
DispatchQueue.main.async {
if mealplanArray.count > 0 {
self.planViewModelDelegate?.plansFetched(self.updateHour(sourcePlans: planViewModel))
}
}
}
})
And this is the one WITHTOUT:
listener = db.collection("Placed_Orders").whereField("userId", isEqualTo: userId).whereField("status", isLessThan: 410).order(by: "status", descending: false).order(by: "nextOrderDate", descending: false).addSnapshotListener({ (documentSnapshot, error) in
if let error = error {
print("error")
self.orderCallback?(error.localizedDescription, nil, .error)
} else {
print("empty")
guard let documents = documentSnapshot?.documents else {
return }
if documents.isEmpty {
self.orderCallback?("No orders found", nil, .error)
return
} else {
print("snapshot count: \(documents.count)")
let orderArray: [Order] = documents.compactMap { queryDocumentSnapshot -> Order? in
return try? queryDocumentSnapshot.data(as: Order.self)
}
let orderViewModel = orderArray.map({ return OrderViewModel(order: $0)})
print("array count: \(orderArray.count)")
DispatchQueue.main.async {
print("status: \(orderViewModel)")
self.orderCallback?(nil, orderViewModel, .success)
}
}
}
})
The differences are rather subtle. In both cases I am using a snapshot listener to query the snapshot and populate it into an array and then map that array into a view model.
Yet, in the latter case, I have to comment out the case id for the identifiable field. I need the ID so need to see if it's working but would like to know why I have to comment out the case id.

How can I get Document Data and Reference Data at same time ?(Firestore, Swift)

I'm trying to get Document Data and Reference Data at the same time, and put data in to custom struct which consist with String Array.
When I run code below, only DocumentReference type "item1_" and "item2_" are appended.
Collection "section" has several documents. Each document has 2 Document references.
I can get right DocumentReference but I can't read it's data in same function.
How can I get both Data from Firestore?
func getall_sec(top: String, mid: String){ref.collection("top_hierarchy").document(top).collection("mid_hierarchy").document(mid).collection("section").addSnapshotListener(){ (snap, err) in
guard let docs = snap else {
self.nosecs = true
return
}
if docs.documentChanges.isEmpty{
self.nosecs = true
return
}
docs.documentChanges.forEach { (doc) in
if doc.type == .added{
let item1_ = doc.document.data()["item1"] as! DocumentReference
let item2_ = doc.document.data()["item2"] as! DocumentReference
item2_.getDocument(){ (querySnapshot, err) in
if let err = err {
print("Error getting documents: \(err)")
} else {
self.item2_name = querySnapshot?.data()?["low"] as! String
self.item2_ImageName = querySnapshot?.data()?["ImageName"] as! String
}
}
item1_.getDocument(){ (querySnapshot, err) in
if let err = err {
print("Error getting documents: \(err)")
} else {
self.item1_name = querySnapshot?.data()?["low"] as! String
self.item1_ImageName = querySnapshot?.data()?["ImageName"] as! String
}
}
self.sections.append(SectionModel(id: doc.document.documentID, item_name1: self.item1_name, item_ImageName1: self.item1_ImageName, item_name2: self.item2_name, item_ImageName2: self.item2_ImageName))
}
}
}
}
The main issue is that getDocument is asynchronous and the the self.sections.append code will actually execute before the code in the closures following the two get functions.
item2_.getDocument(){ (querySnapshot2, err2) in
//this code will execute at some point
}
item1_.getDocument(){ (querySnapshot1, err1) in
//this code will execute at some point
}
//this code will execute before the code in the two above getDocument closures.
self.sections.append(SectionModel(id: doc.document.documentID...
one solution is to nest the calls so they execute in order (this is not the best solution but demonstrates the concept)
item2_.getDocument(){ (querySnapshot, err) in
item1_.getDocument(){ (querySnapshot, err) in
self.sections.append(SectionModel(id: doc.document.documentID...
}
}