Firebase Firestore not working in Swift Share Extension - swift

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

Related

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

Why does Firestore think it retrieved a document when one doesn't exist?

I'm using swift(UI) with firebase and Google SignIn. So far sign in has been great but when I come to using a new user the code below fails - no fatal errors just doesn't add a new user document to Firestore because it seems to think it has retrieved a document which it couldn't because one with the specified ID don't exist.
My guess is the mistake is in the section:
if let error = error as NSError? {
print("Error getting document: \(error.localizedDescription)")
self.setFirestoreUser()
}
the full function:
func fetchUser(documentId: String) {
let docRef = Firestore.firestore().collection("users").document(documentId)
print("User id: \(documentId) ( via fetchUser )")
docRef.getDocument { document, error in
if let error = error as NSError? {
print("Error getting document: \(error.localizedDescription)")
self.setFirestoreUser()
}
else {
if let document = document {
do {
print("Working on coding to User.self")
self.appUser = try document.data(as: User.self)
self.fetchSites()
}
catch {
print("func - fetchUser() error: \(error)")
}
}
}
}
}
The argument 'documentId' is passed on from the google sign process
followup func to create the new Firestore document for this new user:
func setFirestoreUser() {
let googleUser = GIDSignIn.sharedInstance.currentUser
let db = Firestore.firestore()
self.appUser.emailAddress = googleUser?.profile?.email ?? "Unknown"
self.appUser.userGivenName = googleUser?.profile?.givenName ?? ""
self.appUser.userFirstName = googleUser?.profile?.name ?? ""
self.appUser.userProfileURL = googleUser?.profile!.imageURL(withDimension: 100)!.absoluteString ?? ""
do {
try db.collection("users").document(googleUser?.userID ?? "UnknownID").setData(from: self.appUser)
self.fetchUser(documentId: googleUser?.userID ?? "")
} catch {
print(error)
}
}
Calling getDocument on a reference to a non-existing document is not an error (as far as I know), and will return a DocumentSnapshot. To detect whether the document exists, check the exists property on the snapshot instead of (only) checking for errors.
if let document = document {
if !document.exists {
...

How can I create a function to check if a user is blocked?

My app is being prepared for apple app store distribution and the last thing that I need to do is allow users to block another user. My idea was that in a user on firebase, there would be a collection called blocked and it would have documents with the name of blocked users, then whenever a user opens a profile, the app checks if the user of that profile is blocked, and if they are blocked, it just shows a title saying that the user is blocked, instead of showing the regular profile info.
Here is what the code that adds the blocked user to the list looks like:
public func BlockUser(username: String) {
let currentUser = UserDefaults.standard.value(forKey: "username") as! String
let ref = database.collection("users")
.document(currentUser)
.collection("blocked")
.document(username)
ref.setData(["username": username])
}
And that works because this is what shows up in firestore:
So then to do the second part (checking if a profile user is in the blocked list) I use this code:
public var userIsBlocked: Bool = false
public func IsBlockedByUser(username: String) {
let currentUser = UserDefaults.standard.value(forKey: "username") as! String
let ref = database.collection("users")
.document(currentUser)
.collection("blocked")
.document(username)
ref.getDocument { (document, error) in
if let document = document, document.exists {
self.userIsBlocked = true
} else {
print("Document does not exist")
self.userIsBlocked = false
}
}
}
However, even if a user is on the blocked list when I open their profile, it still prints "Document does not exist.". I used googles sample code to look for the document so why is it not finding it?
I still don't see an issue with the code I had previously used but I replaced it with a query and this seems to have fixed the issue. Here's the new code I'm using to search for if a user is blocked:
public var userIsBlocked: Bool = false
public func UserIsBlocked(username: String) {
let currentUser = UserDefaults.standard.value(forKey: "username") as! String
let ref = database.collection("users")
.document(currentUser)
.collection("blocked")
ref.whereField("username", isEqualTo: username)
.getDocuments() { (querySnapshot, err) in
if let err = err {
print("Error getting documents: \(err)")
self.userIsBlocked = false
} else {
for document in querySnapshot!.documents {
print("\(document.documentID) => \(document.data())")
self.userIsBlocked = true
}
}
}
}
public var blockedByUser: Bool = false
public func IsBlockedByUser(username: String) {
let currentUser = UserDefaults.standard.value(forKey: "username") as! String
let ref = database.collection("users")
.document(currentUser)
.collection("blockedBy")
ref.whereField("username", isEqualTo: username)
.getDocuments() { (querySnapshot, err) in
if let err = err {
print("Error getting documents: \(err)")
self.blockedByUser = false
} else {
for document in querySnapshot!.documents {
print("\(document.documentID) => \(document.data())")
self.blockedByUser = true
}
}
}
}
And then when a user's profile is opened, these functions are called to check if the current user has either blocked the user whose profile they are trying to view or have been blocked by them. Based on the result, the two variables are set to either true or false. If they are both false, the profile loads, and if either of them is true, the profile does not load and shows an alert before returning to the previous view controller. This code is all over the place but it gets the job done!

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.

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