Delete document from cloud firestore - swift

I am trying to implement a function that allows the user to delete chats that they have with other users.
At the moment the code works and the firestore document is deleted successfully however as soon as the delete happens the code crashes and i get this error "Fatal error: Unexpectedly found nil while unwrapping an Optional value" next to the id = data!["id"] in the code. I guess that after the delete happens firestore keeps listening for documents and finds an empty collection following the delete. Does anyone know how to stop this from happening?
public func deleteConversation(conversationId: String, completion: #escaping (Bool) -> Void) {
// Get all conversations for current user
let CurrentUser = Auth.auth().currentUser?.uid
let db = Firestore.firestore()
let ConversationRef = db.collection("users").document(CurrentUser!).collection("conversations").document(test!)
print("deleting conversation with \(test!)")
ConversationRef.addSnapshotListener { snapshot, error in
guard let document = snapshot else {
print("Error getting documents: \(String(describing: error))")
return
}
let data = document.data()
if let id = data!["id"] as? String, id == conversationId {
print("conversation found")
ConversationRef.delete()
}
completion(true)
print("deleted conversation")
}
}

The problem comes from:
ConversationRef.addSnapshotListener { snapshot, error in
By calling addSnapshotListener you're adding a listener that:
Gets the document snapshot straight away,
Continues listening for changes and calls your code again then there are any.
The problem is in #2, as it means your code executes again when the document is deleted, and at that point document.data() will be nil.
The simplest fix is to only read the document once:
ConversationRef.getDocument { (document, error) in

Related

Firestore Geohash Query with Live Updating Results in SwiftUI

I'm trying to build an iOS app in SwiftUI where users can find a "Post" near to their current location. I have a sub collection called Posts with a geohash. Somewhat annoyingly this library by google has been archived https://github.com/firebase/geofire-objc for no reason. Instead I had to use this library https://github.com/emilioschepis/swift-geohash.
I find all the neighboring geohashes around the current user and then run a query against firstore for each geohash starting with geohash and ending with geohash + '~'.
Here is the function I wrote:
// import https://github.com/emilioschepis/swift-geohash
class FirestorePosts: ObservableObject {
#Published var items = [FirestorePost]() // Reference to our Model
func geoPointQuery(tag:String){
do {
let db = Firestore.firestore().collection("tags")
let docRef = db.document(tag).collection("posts")
// users current location is "gcpu"
let neighbors = try Geohash.neighbors(of: "gcpu", includingCenter: true)
let queries = neighbors.map { bound -> Query in
let end = "\(bound)~"
return docRef
.order(by: "geohash")
.start(at: [bound])
.end(at: [end])
}
func getDocumentsCompletion(snapshot: QuerySnapshot?, error: Error?) -> () {
guard let documents = snapshot?.documents else {
print("Unable to fetch snapshot data. \(String(describing: error))")
return
}
self.items += documents.compactMap { queryDocumentSnapshot -> FirestorePost? in
return try? queryDocumentSnapshot.data(as: FirestorePost.self)
}
}
for query in queries {
print("ran geo query")
query.getDocuments(completion: getDocumentsCompletion)
}
}
catch{
print(error.localizedDescription)
}
}
}
So far the query works and returns items as expected. However, the results are not updated in realtime when there is a change in Firestore.
How could I make this query update results in realtime? I tried adding query.addSnapshotListener instead but it doesn't like "completion:" parameter
How can I ensure that all the queries are finished before returning the results
You're calling query.getDocuments, which gets data once. If you want to also get updates to that data, you should use addSnapshotListener which listens for updates after getting the initial docs.
To ensure all queries are finished, you could keep a simple counter that you increase each time your addSnapshotListener callback is invoked. When the counter is equal to the number of queries, all of them have gotten a response from the server. That's exactly what the geofire-* libraries for Realtime Database do for their onReady event.
I refactored to this and it seems to work and updates in realtime. I didn't need to use a counter since Im appending the documents to self.items (not sure if thats correct though).
...
for query in queries {
query.addSnapshotListener { (querySnapshot, error) in
guard let documents = querySnapshot?.documents else {
print("No documents")
return
}
self.items += documents.compactMap { queryDocumentSnapshot -> FirestorePost? in
return try? queryDocumentSnapshot.data(as: FirestorePost.self)
}
}
}

Snapshot listener, get the document name, Firebase Swift

I am adding a snapshot listener to a firebase Collection/Document/Collection to get the data as it is updated
The issue I am having is I run through a for loop and as it gets the i in the for loop I then use that string (collection name), to direct the snapshot listener, when the data comes in it will add to the data already there rather than change the data. because it doesn't touch the code to get the collection name, as far as I know.
What I need to do is to be able to add the data to a dictionary that has [String:Any], so I can have the ["collection name":name, "document name": document id, "info":document data], this is fine on first run but when data is changed it only get the data from the changed listener but I don't know how I can get it to get the collection name so I can remove that document from the dictionary before adding the new data.
func getClockOutData(completion: #escaping (_ finished: Bool) -> ()) {
for i in projectsList {
Firestore.firestore().collection("Projects").document(i).collection("TimeSheets").addSnapshotListener { (snapshot, err) in
if let err = err {
print("Tony the error was \(err.localizedDescription)")
} else {
print("Tony dataC A get is \(i)")
if let projDocs = snapshot?.documents {
print("Tony dataC get is \(i)")
for d in projDocs {
let dataG = d.data()
let dateG = d.documentID
let dataCT = ["project":i, "dateG":dateG, "inf":dataG] as [String : Any]
print("Tony dataC is \(dataCT)")
self.dataC.append(dataCT)
}
completion(true)
}
}
}
}
}
How can I get the project name (i) when the snapshot fires again with the changes?
I want that so I can create a for loop to check the project and add all the data from the project to a dict with all the same project name grouped and then run through that loop when there are changes to remove the project info before it is re appended
When you're retrieving the documents from the subcollection, no data from the parent document is retrieved.
So you will either have to load the parent document to get that information, or get the information from the context where you make this request (as likely you've loaded the parent document before as part of determining projectsList).

Not receiving error information from read data in firebase using swift

I have managed to get firebase working and to read data from the database and present it.
I'm taking pictures and getting CoreML to work out what the item is then sending it to the database to return data on the item.
If the item is not in the database I, therefore want this to error but instead, the return is just blank. It seems like the firebase error block isn't working at all as it doesn't get to if after executing the first part of the code.
I have tried using a do catch block also but with no luck.
Please see the code attached:
ref.child("items").child("\(self.final)").observeSingleEvent(of: .value, with: { (snapshot) in
// Get item value
let value = snapshot.value as? String ?? ""
print(value)
self.calorieCount.text = "\(value)"
}) { (error) in
print(error.localizedDescription)
print("error")
self.calorieCount.text = "Item not found, you will be able to add this soon"
}
}
Would somebody be able to tell me why the error part doesn't work when the item isn't in the database?
Thanks in advance!
Not having data at a location is not considered an error in the Firebase API, so the error closure doesn't get called. Instead your regular closure is called with an empty DataSnapshot, for which you can test with:
ref.child("items").child("\(self.final)").observeSingleEvent(of: .value, with: { (snapshot) in
if !snapshot.exists() {
self.calorieCount.text = "Item not found, you will be able to add this soon"
}
else {
let value = snapshot.value as? String ?? ""
self.calorieCount.text = "\(value)"
}
})

Extremely confused by the scope of Firestore GetDocuments request and the in loop of Swift

All that I'm trying to do is to check whether the value for a 'key' exists in a Firestore collection as a document and return a Bool.
But I can't seem to return anything within the getDocument,
so I thought that I should then keep a results var and update the results var but the changes I make to results don't stick and it stays default false.
How do I simplify this whole mess?
func checkIfValid(db: Firestore, key: String) -> Bool {
let resolve = db.collection("keys").document(key)
var results = false
resolve.getDocument{ (document, error) in
if let document = document, document.exists {
var results = true
} else { results = false }
}
print(results)
return results
}
Reading the firebase docs, they have a small warning below the sample code.
Note: If there is no document at the location referenced by docRef,
the resulting document will be empty and calling exists on it will
return false.
However, you need to add a completion handler to the function given you're working with network requests. Swift will return the result variable given you specified it; ignoring any response from the getDocuments handler.
I changed the function to fix your mess.
func checkIfValid(db: Firestore, key: String, completion: #escaping(Bool) -> ()) {
let docRef = db.collection("user").document(key)
docRef.getDocument { (document, error) in
if let document = document, document.exists {
print("document exists.")
completion(true)
} else {
print("document does not exists.")
completion(false)
}
}
}
And to get the response, just use this.
checkIfValid(db: db, key: "", completion: {success in
print(success)
})

Listen to any new document added to a collection in firestore

The existing solutions are not working for me, I've tried. What I'm trying to do is to fetch messages in a thread that is stored on Firestore. This is the current structure of Chat collection:
As you can see this chat between user 1 and user 2 only has 2 messages and I'm trying to fetch these and also listen to new ones if they are added to thread. Here is the code of how I'm doing it:
func loadChat() {
self.showWaitOverlayWithText("Loading")
let db = Firestore.firestore().collection("Chats")
.whereField("user1ID", isEqualTo: Auth.auth().currentUser?.uid ?? "Not Found User 1")
.whereField("user2UID", isEqualTo: user2UID ?? "Not Found User 2")
db.getDocuments { (querySnap, error) in
if let error = error {
print("Error: \(error)")
return
} else {
let doc = querySnap?.documents.first
doc?.reference.collection("thread").addSnapshotListener(includeMetadataChanges: true, listener: { (threadQuery, error) in
//Never comes here
for message in threadQuery!.documents {
self.removeAllOverlays()
print("Data: \(message.data())")
}
})
}
}
}
It never executes collection("thread").addSnapshotListner.
So, the issue was simply an incorrect key in the query, "user2UID" instead of "user2ID". We were able to pinpoint this issue by checking whether or not the optional values (doc, querySnap) were nil or not. doc was equal to nil, querySnap was not, but its document count was equal to zero. This helped us know that the issue was with the line
let db = Firestore.firestore().collection("Chats")
.whereField("user1ID", isEqualTo: Auth.auth().currentUser?.uid ?? "Not Found User 1")
.whereField("user2UID", isEqualTo: user2UID ?? "Not Found User 2")
I initially thought the issue would have been an inconsistency with the values, not the keys, on the backend. A second look by Chaundhry confirmed that there was an incorrect key.