Removing an array item from Firestore - swift

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.

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

Swift for loop not waiting for firestore call to complete

I know firestore calls are async which means this will not work:
private func removeUserSavedMomentFromAllUsers(moment: StoryMoment, completion: #escaping () -> Void) {
guard let savedByUIDs = moment.savedByUIDs else { return }
guard let momentID = moment.id else { return }
for id in savedByUIDs {
self.userInfoCollection.document(id).collection("savedMedias").document(momentID).delete { error in
if let error = error {
print("Error removing user saved moment from UID: \(error)")
}
}
}
}
Since the loop will continue before the delete call completes (same with get requests). I have used dispatch groups in the past to solve this issue. Heres a working example:
private func removeUserSavedMomentFromAllUsers(moment: StoryMoment, completion: #escaping () -> Void) {
guard let savedByUIDs = moment.savedByUIDs else { return }
guard let momentID = moment.id else { return }
let disSemaphore = DispatchSemaphore(value: 0)
let dispatchQueue = DispatchQueue(label: "group 1")
dispatchQueue.async {
for id in savedByUIDs {
self.userInfoCollection.document(id).collection("savedMedias").document(momentID).delete { error in
if let error = error {
print("Error removing user saved moment from UID: \(error)")
} else {
disSemaphore.signal()
}
}
disSemaphore.wait()
}
}
}
But those do all the work on the background thread.
My question is: How can I use async/await in a for loop where you call firebase docs?
The code in the first part of the question does work - and works fine for small group of data. However, in general it's recommended to not call Firebase functions in tight loops.
While the question mentions DispatchQueues, we use DispatchGroups with .enter and .leave as it's pretty clean.
Given a Firebase structure
sample_data
a
key: "value"
b
key: "value"
c
key: "value"
d
key: "value"
e
key: "value"
f
key: "value"
and suppose we want to delete the d, e, and f documents. Here's the code
func dispatchGroupDelete() {
let documentsToDelete = ["d", "e", "f"]
let collection = self.db.collection("sample_data") //self.db points to my Firestore
let group = DispatchGroup()
for docId in documentsToDelete {
group.enter()
let docToDelete = collection.document(docId)
docToDelete.delete()
group.leave()
}
}
While this answer doesn't use async/await, those may not be needed for this use case
If you want to use async/await you try do this
let documentsToDelete = ["d", "e", "f"]
let collection = self.db.collection("sample_data")
for docId in documentsToDelete {
let docToDelete = collection.document(docId)
Task {
do {
try await docToDelete.delete()
} catch {
print("oops")
}
}
}

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.

Firebase Storage listAll() body not executed

I am new to Firebase and Swift. My previous question was very vague due to a misunderstanding on my part. In a class named "A" for example I am trying to create an object of class "B" that contains the fetchARImageTargets function that I have below. I am trying to assign the array ARImageTargets to a var in class "A" however, the listAll completion is not returned in time, which results in the var being empty. Is there a way that I can edit my function or class to avoid the var being set prematurely?
let ARImageTargetStorageRef = Storage.storage().reference().child("ImageTargets")
self.fetchARImageTargets(ref: ARImageTargetStorageRef)
func fetchARImageTargets(ref: StorageReference) {
ref.listAll { (result, error) in
if let error = error {
print(error)
}
for prefix in result.prefixes {
self.fetchARImageTargets(ref: prefix)
}
for item in result.items {
item.getMetadata { (metadata, error) in
if let error = error {
print(error)
} else {
var imageTarget = ARImageTarget()
item.downloadURL(completion: { (url, error) in
imageTarget.ImageURL = url
})
imageTarget.Id = metadata?.customMetadata?["Id"] as String?
let width = metadata?.customMetadata?["PhysicalWidth"] as String?
imageTarget.PhysicalWidth = CGFloat(truncating: NumberFormatter().number(from: width!)!)
self.ARImageTargets.append(imageTarget)
}
}
}
}
}

Firebase Firestore not working in Swift Share Extension

I am building a Share Extension in Swift which saves a document to Firestore. So far I have been able to authenticate the correct user via keychain sharing and app groups. I can also get a documentID from a new document reference:
var ref = Firestore.firestore().collection("stuff").document()
print(ref.documentID) //prints the id
But when I try to save something to Firestore, nothing prints in the console, meaning I get neither a failure or success callback from Firebase (see below where I batch the updates). Here is my ShareController.swift file:
class ShareViewController: SLComposeServiceViewController {
var sharedIdentifier = "asdf"
override func viewDidLoad() {
FirebaseApp.configure()
setupKeychainSharing()
}
func setupKeychainSharing() {
do {
try Auth.auth().useUserAccessGroup(sharedIdentifier)
} catch let error as NSError {
}
}
override func isContentValid() -> Bool {
return true
}
override func didSelectPost() {
if let content = extensionContext!.inputItems[0] as? NSExtensionItem {
if let contents = content.attachments {
for attachment in contents {
if attachment.hasItemConformingToTypeIdentifier(m4aType) {
attachment.loadItem(forTypeIdentifier: m4aType, options: nil, completionHandler: { (results, error) in
if error == nil {
if let url = results as? URL {
if let audioData = NSData(contentsOf: url) {
let fileName = url.lastPathComponent
if Auth.auth().currentUser != nil {
guard let myId = Auth.auth().currentUser?.uid else { return }
let batch = Firestore.firestore().batch()
let ref = Firestore.firestore().collection("projects").document()
let project: [String: Any] = [
"ownerId": myId,
"type" : "audio",
"extensionUrl" : audioUrl.absoluteString
]
batch.updateData(project, forDocument: ref)
let privateRef = Firestore.firestore().collection("user-private").document(myId)
let privateUpdate: [String: Any] = [
"projects" : FieldValue.arrayUnion([ref.documentID])
]
batch.updateData(privateUpdate, forDocument: privateRef)
batch.commit(completion: { (error) in
if let error = error {
print("error updating database: \(error.localizedDescription)")
} else {
print("Database updated successfully!!!!!")
self.extensionContext!.completeRequest( returningItems: [], completionHandler: nil)
}
})
}
}
}
}
})
}
}
}
}
}
}
It appears you're trying to create additional documents within the projects node and update the user-private node. If so, the code in the question won't work.
UpdateData: Updates fields in the document referred to by document. If
document does not exist, the write batch will fail.
Here's a working batch write function that adds a document to a projects node with a Firestore generated document ID and child fields for extension, ownerId and type as well as a user_private collection with a document with a documentId of id_0.
func batchWrite() {
let batch = self.db.batch()
let ref = self.db.collection("projects").document()
let project: [String: Any] = [
"ownerId": "id_0",
"type" : "audio",
"extensionUrl" : "some url"
]
batch.setData(project, forDocument: ref)
let privateRef = self.db.collection("user-private").document("id_0")
let privateUpdate: [String: Any] = [
"projects" : "some projects"
]
batch.setData(privateUpdate, forDocument: privateRef)
batch.commit(completion: { (error) in
if let error = error {
print("error updating database: \(error.localizedDescription)")
} else {
print("Database updated successfully!!!!!")
}
})
}
Note that self.db is a class var reference to my Firestore. That makes it so you don't have to keep typing Firestore.firestore() and use self.db instead.
Also note that a batch is probably not needed in this case as it doesn't appear there are a significant number of writes occurring at the same time.
If you're not using batch, the .addDocument will add documents to collections.
Here's a function that writes a task to a tasks collection and auto-generates a documentId
func writeTask() {
let collectionRef = self.db.collection("tasks")
let data = [
"task": "some task"]
var ref: DocumentReference? = nil
ref = collectionRef.addDocument(data: data, completion: { err in
if let error = err {
print(error.localizedDescription)
}
print(ref?.documentID)
})
}
It is likely Firebase wasn't configured for your Share extension. Print statements do not work for share extension. Instead you should make use of NSLog statements i.e NSLog("refDocId:\(ref.DocumentId)"). Then in your xcode, navigate to Window > Devices and Simulators > Open Console. To configure Firebase in your Share extension, use FirebaseApp.configure()