Removing one item from map removes all items - swift

I have a simple Map in my Firestore Database, looks like that:
I'm trying to remove just one item from the following map, like that:
func removeUserFromFriendsList(friendToRemove id: String, _ currentUserId: String, completion: #escaping(Bool)->()) {
let friendsRef = db.collection(USERS_COLLECTION).document(currentUserId)
friendsRef.updateData([
USER_FOLLOWING: FieldValue.arrayRemove([id])
]) { (error) in
if error != nil {
completion(false)
}
completion(true)
}
}
but instead of removing the item with the ID I got, it removes the whole list inside following

I guess the problem was that I was in the following level, and didn't specify the id to delete correctly. so It just deleted everything inside following.
Specifying the id and deleting it like that did the trick:
func removeUserFromFriendsList(friendToRemove id: String, _ currentUserId: String, completion: #escaping(Bool)->()) {
let friendsRef = db.collection(USERS_COLLECTION).document(currentUserId)
friendsRef.updateData([
USER_FOLLOWING + "." + id : FieldValue.delete()
]) { (error) in
if error != nil {
completion(false)
}
completion(true)
}
}
USER_FOLLOWING is the name of the field in the database (aka, 'following').
Adding the + "."+ and the id just specify which id we want to delete, so it looks likes that:
following.AP3ENXgW2mhvaWsUeDOxchYaAGm1 <--- the field and id to delete
and then using FieldValue.delete to delete it entirely.
EDIT:
Answer found here:
How to remove an array item from a nested document in firebase?

Following the example from the Firebase documentation:
let friendsRef = db.collection(USERS_COLLECTION).document(currentUserId)
// Atomically removes an ID from the "USER_FOLLOWING" array field.
friendsRef.updateData([
"USER_FOLLOWING": FieldValue.arrayRemove([id])
])
If this is not an option for you, a workaround for this would be to read the entire array out of the document, make modifications to it in memory, then update the modified array field entirely.

Related

Swift: How do I repeat a getDoc request to Firestore?

I can already check if a doc exists in a collection. However I am unable to repeatedly check the same collection while trying different path names.
For example, my collection name is the UID of the user. There can be an unlimited amount of docs in this collection. The docs are titled "UID-0", "UID-1", "UID-2" and so on as the user adds items.
Every time it finds a doc that already exists such as "UID-0" it will change the path request to "UID-+=1" until the number exceeds the docs and it is able to create and use that path name.
Each doc contains about a dozen fields of the same data model but of course different data.
var docAlreadyExists: Bool = true
var multipleUserFencesIdCount: Int = 0
var newID: String = ""
let id = self.auth.currentUser?.uid ?? ""
repeat {
print("1")
self.fencesInfoCollection.document("Live").collection(id).document(newID).getDocument(completion: { document, error in
print("2")
if let document = document, document.exists {
print("EXISTS")
multipleUserFencesIdCount += 1
newID = newID.dropLast() + "\(multipleUserFencesIdCount)"
} else {
print("DOES NOT EXIST")
docAlreadyExists = false
}
})
} while docAlreadyExists
With that said, how can I repeatedly check if a document exists until the path name exceeds the rest and is able to create a new doc with the new data.
Edit:
The 1 gets repeatedly called correctly but the .getDoc never calls since 2 is never printed.
I figured out a better solution to my goal, instead of trying to repeat a call with different IDs I am now getting all documents and counting how many are in the collection.
self.fencesInfoCollection.document(id).collection("Live").getDocuments(completion: { document, error in
if let document = document, document.isEmpty {
print("EMPTY")
} else {
print("DOC1: \(String(describing: document?.count))")
}
})

Removing an array item from Firestore not working when array contains date

I've spent days researching this including various answers like: Firebase Firestore: Append/Remove items from document array and my previous question at: Removing an array item from Firestore
but can't work out how to actually get this working. Turns out the issue is when there is a date property in the object as shown below:
I have two structs:
struct TestList : Codable {
var title : String
var color: String
var number: Int
var date: Date
var asDict: [String: Any] {
return ["title" : self.title,
"color" : self.color,
"number" : self.number,
"date" : self.date]
}
}
struct TestGroup: Codable {
var items: [TestList]
}
I am able to add data using FieldValue.arrayUnion:
#objc func addAdditionalArray() {
let testList = TestList(title: "Testing", color: "blue", number: Int.random(in: 1..<999), date: Date())
let docRef = FirestoreReferenceManager.simTest.document("def")
docRef.updateData([
"items" : FieldValue.arrayUnion([["title":testList.title,
"color":testList.color,
"number":testList.number,
"date": testList.date]])
])
}
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 deleteArray() {
let docRef = FirestoreReferenceManager.simTest.document("def")
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([itemToRemove.asDict])
]) { 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.
I've tried different variations and this code works as long as I don't have a date property in the struct/object. The moment I add a date field, it breaks and stops working. Any ideas on what I'm doing wrong here?
Please note: I've tried passing in the field values as above in FieldValue.arrayUnion as well as the object as per FieldValue.arrayRemove and the same issue persists regardless of which method I use.
The problem is, as you noted, the Date field. And it's a problem because Firestore does not preserve the native Date object when it's stored in the database--they are converted into date objects native to Firestore. And the go-between these two data types is a token system. For example, when you write a date to Firestore from a Swift client, you actually send the database a token which is then redeemed by the server when it arrives which then creates the Firestore date object in the database. Conversely, when you read a date from Firestore on a Swift client, you actually receive a token which is then redeemed by the client which you then can convert into a Swift Date object. Therefore, the definition of "now" is not the same on the client as it is on the server, there is a discrepancy.
That said, in order to remove a specific item from a Firestore array, you must recreate that exact item to give to FieldValue.arrayRemove(), which as you can now imagine is tricky with dates. Unlike Swift, you cannot remove items from Firestore arrays by index. Therefore, if you want to keep your data architecture as is (because there is a workaround I will explain below), the safest way is to get the item itself from the server and pass that into FieldValue.arrayRemove(). You can do this with a regular read and then execute the remove in the completion handler or you can perform it atomically (safer) in a transaction.
let db = Firestore.firestore()
db.runTransaction { (trans, errorPointer) -> Any? in
let doc: DocumentSnapshot
let docRef = db.document("test/def")
// get the document
do {
try doc = trans.getDocument(docRef)
} catch let error as NSError {
errorPointer?.pointee = error
return nil
}
// get the items from the document
if let items = doc.get("items") as? [[String: Any]] {
// find the element to delete
if let toDelete = items.first(where: { (element) -> Bool in
// the predicate for finding the element
if let number = element["number"] as? Int,
number == 385 {
return true
} else {
return false
}
}) {
// element found, remove it
docRef.updateData([
"items": FieldValue.arrayRemove([toDelete])
])
}
} else {
// array itself not found
print("items not found")
}
return nil // you can return things out of transactions but not needed here so return nil
} completion: { (_, error) in
if let error = error {
print(error)
} else {
print("transaction done")
}
}
The workaround I mentioned earlier is to bypass the token system altogether. And the simplest way to do that is to express time as an integer, using the Unix timestamp. This way, the date is stored as an integer in the database which is almost how you'd expect it to be stored anyway. This makes locating array elements that contain dates simpler because time on the client is now equal to time on the server. This is not the case with tokens because the actual date that is stored in the database, for example, is when the token is redeemed and not when it was created.
You can extend Date to conveniently convert dates to timestamps and extend Int to conveniently convert timestamps to dates:
typealias UnixTimestamp = Int
extension Date {
var unixTimestamp: UnixTimestamp {
return UnixTimestamp(self.timeIntervalSince1970 * 1_000) // millisecond precision
}
}
extension UnixTimestamp {
var dateObject: Date {
return Date(timeIntervalSince1970: TimeInterval(self / 1_000)) // must take a millisecond-precision unix timestamp
}
}
One last thing is that in my example, I located the element to delete by its number field (I used your data), which I assumed to be a unique identifier. I don't know the nature of these elements and how they are uniquely identified so consider the filter predicate in my code to be purely an assumption.

Delete a specific value in an array from Firestore Swift

I have this code here and I want to delete certain value inside the array "Answered". Is there a simple way to access the first value in the array? This is right but shows what I want to happen "Answered[0]" <- I want to get the first value in that array and delete it. Thank you in Advance
let uid = Auth.auth().currentUser?.uid
print(self.randomArray)
let wash = db.collection("users").document(uid!)
wash.updateData([
"Answered": FieldValue.arrayUnion([self.randomArray])
])
}
if(self.check.isEmpty != true){
self.whichQuestion = self.check[0]
self.whichQuestionString = String(self.whichQuestion)
db.collection("users").document(uid!).updateData([
"Answered": FieldValue.delete(),
]) { err in
if let err = err {
print("Error updating document: \(err)")
} else {
print("Document successfully updated")
}
}
If your array contains unique values however, you can remove the item with:
self.whichQuestionString = String(self.whichQuestion)
db.collection("users").document(uid!).updateData([
"regions": FieldValue.arrayRemove([whichQuestionString])
])
If you only know the index of the item, there is no way to remove it without knowing the entire array.
The recipe for this is:
Read the document from Firestore
Modify the array in your application code
Write the entire modified array back to Firestore
Also see (none of which unfortunately are for Swift):
Is there any way to update a specific index from the array in Firestore
Delete data from Firestore dynamically

How can I create a snapshot to listen to users being ADDED to my Firestore database?

I am using Firestore. I am trying to listen for when a new user is added. The problem is, each user also has a friends dictionary. So when I use a snapshot, my code is detecting both events of (1) A new user being added and (2) a new friend being added.
I have tries iterating over the document changes data and restricting doc.document.data()["friends"] == nil. Why isn't this working/how can I properly add a restriction to only include when a new user is added?
func observeUsers(onSuccess: #escaping(UserCompletion)) {
Ref().firestoreUserRef.collection("users").addSnapshotListener { (querySnapshot, error) in
if error != nil {
print("error with observeUser snapshot")
return
}
querySnapshot?.documentChanges.forEach { doc in
//I want to detect that a new user was added, I do not want to detect if a friend was added
if (doc.type == .added) && doc.document.data()["friends"] == nil {
guard let dict = querySnapshot else { return }
for document in dict.documents {
var dictionary = [String : Any]()
dictionary = document.data()
if let user = User.transformUser(dict: dictionary) {
onSuccess(user)
}
}
}
}
}
}
The easiest way is starting from the data model to support the types of queries you want. In this case, when you create a new user (which I assume always has no friends), set the friends field explicitly to null. This will let you query for all new users:
Ref().firestoreUserRef.collection("users").whereField("friends", isEqualTo: NSNull()).addSnapshotListener ...
An assumption here is your transformUser process will update the document and replace null with either a list of friends of an empty array so that it no longer matches the query.

How to fix error when looking for child in firebase database (Swift)?

I am trying to save items to a Firebase database under a child taken from a text field. I haven't got to that yet though, I am still trying to work out how to check if a value already exists. Here is my code...
#objc func submitNote() {
print("Attempting to save or update note")
//Check if file exists
if name != nil && text != nil {
//Access database
var ref: DatabaseReference!
ref = Database.database().reference()
ref.child("lists").observeSingleEvent(of: .value, with: { (snapshot) in
if snapshot.hasChild(self.name!){
print("true")
//Update
return
} else {
print("false")
//Create
self.uploadNew()
}
})
return
} else {
print("Error")
return
}
}
It has to be an #objc function as it is run using #selector in a menu bar. As you can see it checks to make sure the text fields aren't empty. Here are the variables...
public var name: String?
public var text: String?
Which are set inside separate functions...
name = titleText.text
text = textView.text
The reason I have set the variables this way is because I am creating the view programically and both individual text views/fields are set inside functions so I don't think I can access them from the submitNote function. Anyway the error I'm getting it this...
*** Terminating app due to uncaught exception 'InvalidPathValidation', reason: '(hasChild:) Must be a non-empty string and not contain '.' '#' '$' '[' or ']''
I have checked and triple checked but there aren't any of those values in the text fields and even though the if statement is meant to stop the app continuing unless they are not equal to nil I tried with text in the fields but no matter what if I press the button to run this function I get the same error. Does anyone know how to fix it?
You only check if name is not null but not if it is empty ("")
if let value = self.name, !value.isEmpty...
Your Firebase structure was not included in the question so I'll make one up for you
lists
Leroy: true
Billy: true
James: true
now some code to see if a name exists in the lists node. We're using snapshot.exists to see if any data was returned in the snapshot.
func doesNameExistInLists(name: String) {
let ref = self.ref.child("lists").child(name)
ref.observeSingleEvent(of: .value, with: { snapshot in
if snapshot.exists() {
print("found it")
} else {
print("name not found")
}
})
}
then when it's called, here are the results
self.doesNameExistInLists(name: "Billy")
console: found it
self.doesNameExistInLists(name: "aaaa")
console: name not found