SiwftUI Firebase modifying variable inside snapshot change not working - swift

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())")
}
}
}
}

Related

How to wait for code inside addSnapshotListener to finish execution before returning to function?

func getStudents() {
var student: Student = Student()
db.collection(StudentViewModel.studentCollection).addSnapshotListener { (querySnapshot, error) in
guard error == nil else {
print("ERROR | Getting Student Documents in Firestore Service: \(String(describing: error))")
return
}
guard let snapshot = querySnapshot else {
// no documents
print("No documents to fetch!")
return
}
DispatchQueue.main.sync {
var updatedStudentDocuments: [Student] = [Student]()
for studentDocument in snapshot.documents {
student = Student(id: studentDocument.documentID,
name: studentDocument.data()["name"] as? String ?? "")
updatedStudentDocuments.append(student)
}
self.students = updatedStudentDocuments
}
}
}
Every time I run this function and check what's inside self.students, I find that it's empty. That's because the function getStudents is returning before the closure in addSnapshotListener finishes executing. How do I make the getStudents function wait for the closure to finish executing before continuing its own execution?

Deleting Firebase values on a SwiftUI List

I am trying to delete a value from the Firebase Realtime Database, just by swiping on a the list row but I am encountering a lot of problems, for example, when I delete a row, on the Firebase Realtime Database an object with a different id is deleted, I have the following code running when the swipe occurs (Sorry for the lot of prints but I was trying to understand the flow on my program :D):
EDIT: I am wondering how I can retrieve the value of the row in the List and by that using this extension:
extension Dictionary where Value: Equatable {
func someKey(forValue val: Value) -> Key? {
return first(where: { $1 == val })?.key
}
}
And this is the delete code.
.onDelete(perform: { indexSet in
locali.remove(atOffsets: indexSet)
Auth.auth().addStateDidChangeListener { auth, user in
print(auth.currentUser?.email)
print(indexSet.count)
print(indexSet.startIndex)
print(indexSet.endIndex)
var indexCounter: Int = indexSet.first!
//MARK: - delete
ref
.child("locali")
.child(auth.currentUser?.uid ?? "no uid")
.child("nomeLocale\(indexCounter)")
.removeValue (completionBlock: { (error, refer) in
if error != nil {
print(error)
} else {
print(refer)
print("Child Removed Correctly")
}
})
//MARK: - refactor
Task {
var dataSnapshot: DataSnapshot
var refactoredWholeList: [String : NSObject]
do {
dataSnapshot = try await ref
.child("locali")
.child(auth.currentUser?.uid ?? "no uid")
.getData()
print(dataSnapshot)
guard var wholeList = dataSnapshot.value as? [String : NSObject] else {
print("error with the guard first inside onDelete")
return
}
var indexCounterDelete: Int = 0
print("before changing indexes ----> \(wholeList) <----\n")
for indexDelLocale in wholeList {
print("This is the indexDelLocale ==> \(indexDelLocale)")
print("indexCounterDelete: \(indexCounterDelete)")
if var entry = wholeList.removeValue(forKey: indexDelLocale.key) {
print("Trying to sobstitute the index: \(indexDelLocale.key)")
print("With: \("nomeLocale\(indexCounterDelete)") for ==> \(entry)")
try await ref
.child("locali")
.child(auth.currentUser?.uid ?? "no uid")
.updateChildValues(["nomeLocale\(indexCounterDelete)" : entry])
}
print("List count ==> \(wholeListRefactored.count)\n")
if wholeList.count == indexCounterDelete {
print("RETURN")
} else {
indexCounterDelete += 1
}
}
print("\nafter changing indexes ----> \(wholeList) <----")
print("This is the wholeListRefactored ==> \(wholeListRefactored)\n")
} catch {
print("error while retrieving the wholeList")
}
} //: Task
// //MARK: - reupload
// for locale in wholeListRefactored {
// ref
// .child("locali")
// .child(auth.currentUser?.uid ?? "no uid")
// .updateChildValues([locale.key : locale.value])
// }
}
}) //: onDelete

Removing an array item from Firestore

I've spent days researching this including various answers like: Firebase Firestore: Append/Remove items from document array
but can't work out how to actually get this working.
I have two structs:
struct TestList : Codable {
var title : String
var color: String
var number: Int
}
struct TestGroup: Codable {
var items: [TestList]
}
I am able to add data using FieldValue.arrayUnion:
#objc func addNewItem() {
let testList = TestList(title: "Testing", color: "blue", number: Int.random(in: 1..<999))
let docRef = FirestoreReferenceManager.simTest.document("abc")
docRef.updateData([
"items" : FieldValue.arrayUnion([["title":testList.title,
"color":testList.color,
"number":testList.number]])
])
}
The above works as reflected in the Firestore dashboard:
But if I try and remove one of the items in the array, it just doesn't work.
#objc func removeItem() {
let docRef = FirestoreReferenceManager.simTest.document("abc")
docRef.getDocument { (document, error) in
do {
let retrievedTestGroup = try document?.data(as: TestGroup.self)
let retrievedTestItem = retrievedTestGroup?.items[1]
guard let itemToRemove = retrievedTestItem else { return }
docRef.updateData([
"items" : FieldValue.arrayUnion([["title" : itemToRemove.title,
"color" : itemToRemove.color,
"number" : itemToRemove.number]])
]) { error in
if let error = error {
print("error: \(error)")
} else {
print("successfully deleted")
}
}
} catch {
}
}
}
I have printed the itemToRemove to the log to check that it is correct and it is. But it just doesn't remove it from Firestore. There is no error returned, yet the "successfully deleted" is logged.
Note the above is test code as I've simplified what I actually need just for testing purposes because I can't get this working.
Any ideas on what I'm doing wrong here?
You have to use arrayRemove to remove items from arrays.
#objc func removeItem() {
let docRef = FirestoreReferenceManager.simTest.document("abc")
docRef.getDocument { (document, error) in
do {
let retrievedTestGroup = try document?.data(as: TestGroup.self)
let retrievedTestItem = retrievedTestGroup?.items[1]
guard let itemToRemove = retrievedTestItem else { return }
docRef.updateData([
"items" : FieldValue.arrayRemove([["title" : itemToRemove.title,
"color" : itemToRemove.color,
"number" : itemToRemove.number]])
]) { error in
if let error = error {
print("error: \(error)")
} else {
print("successfully deleted")
}
}
} catch {
}
}
}
I've encountered situations where this straightforward approach didn't work because the item was a complex object, in which case I first had to query for the item from Firestore and plug that instance into arrayRemove() to remove it.
The reason your approach doesn't have any side effects is because arrays in Firestore are not like arrays in Swift, they are hybrids of arrays and sets. You can initialize an array in Firestore with duplicate items but you cannot append arrays using arrayUnion() with duplicate items. Trying to append a duplicate item using arrayUnion() will silently fail, such as in your case.

Firestore Querying a Snapshot within a Snapshot?

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)

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.