Code not executing in docRef.getDocument? - swift

I have a function to check whether an account already exists in the firestore database so that the user can register for one. I have an bool exists to return whether the account exists or not but the code in getDocument() is not executing and assigning the values properly.
func checkAccountExists(username: String) -> Bool {
let docRef = accountsRef!.document(username)
var exists: Bool!
docRef.getDocument { (document, error) in
if let document = document, document.exists {
exists = true
} else {
exists = false
}
}
return exists //Unexpectedly found nil error
}

Not only does Doug correctly point out that the function is async, in Swift, you should not attempt a function (or a computed property) that returns an async value. Instead, consider a function with an async completion that passes the value as an argument.
func checkAccountExists(username: String, completion: #escaping (_ exists: Bool) -> Void) {
accountsRef!.document(username).getDocument { (document, error) in
if let document = document,
document.exists {
completion(true)
} else {
if let error = error {
print(error)
}
completion(false)
}
}
}
Usage
checkAccountExists(username: someUsername) { (exists) in
if exists {
print("user exists")
} else {
print("user doesn't exist or there was an error")
}
}
However, since the database could return an error (even if the user exists), consider returning a Result instead of a Bool. But if you just want a pass/fail mechanism that doesn't decipher between errors and a user truly not existing, this will work.

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?

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 {
...

Future Combine sink does not recieve any values

I want to add a value to Firestore. When finished I want to return the added value. The value does get added to Firestore successfully. However, the value does not go through sink.
This is the function that does not work:
func createPremium(user id: String, isPremium: Bool) -> AnyPublisher<Bool,Never> {
let dic = ["premium":isPremium]
return Future<Bool,Never> { promise in
self.db.collection(self.dbName).document(id).setData(dic, merge: true) { error in
if let error = error {
print(error.localizedDescription)
} else {
/// does get called
promise(.success(isPremium))
}
}
}.eraseToAnyPublisher()
}
I made a test function that works:
func test() -> AnyPublisher<Bool,Never> {
return Future<Bool,Never> { promise in
promise(.success(true))
}.eraseToAnyPublisher()
}
premiumRepository.createPremium(user: userID ?? "1234", isPremium: true)
.sink { receivedValue in
/// does not get called
print(receivedValue)
}.cancel()
test()
.sink { recievedValue in
/// does get called
print("Test", recievedValue)
}.cancel()
Also I have a similar code snippet that works:
func loadExercises(category: Category) -> AnyPublisher<[Exercise], Error> {
let document = store.collection(category.rawValue)
return Future<[Exercise], Error> { promise in
document.getDocuments { documents, error in
if let error = error {
promise(.failure(error))
} else if let documents = documents {
var exercises = [Exercise]()
for document in documents.documents {
do {
let decoded = try FirestoreDecoder().decode(Exercise.self, from: document.data())
exercises.append(decoded)
} catch let error {
promise(.failure(error))
}
}
promise(.success(exercises))
}
}
}.eraseToAnyPublisher()
}
I tried to add a buffer but it did not lead to success.
Try to change/remove .cancel() method on your subscriptions. Seems you subscribe to the publisher, and then immediately cancel the subscription. The better option is to retain and store all your subscriptions in the cancellable set.

How to return expected value from within another function in Swift? [duplicate]

This question already has an answer here:
I want to return Boolean after firebase code is executed
(1 answer)
Closed 3 years ago.
I have a function that returns a boolean to check if a user already voted on post. However, I'm struggling to get the correct boolean to return. I run a Firebase query to check the data in the backend but the defaul boolean of false is always returned. What's the best way to sort this logic out?
I understand why it's defaulting to false: I set it above the block, and then the code hits the return false before the query can complete. What's the best approach?
func didAlreadyVote(message: MessageType) -> Bool {
// check user votes collection to see if current message matches
guard let currentUser = Auth.auth().currentUser else {return false}
let userID = currentUser.uid
var bool = false
let docRef = Firestore.firestore().collection("users").document(userID).collection("upvotes").whereField("messageId", isEqualTo: message.messageId)
docRef.getDocuments { querySnapshot, error in
if let error = error {
print("Error getting documents: \(error)")
bool = false
} else {
for document in querySnapshot!.documents {
print("\(document.documentID) => \(document.data())")
bool = true
}
}
}
return bool
}
You are returning before the closure got a chance to complete, hence, the return value is false. To solve this, you can pass another closure in the function signature:
func didAlreadyVote(message: MessageType, completion: (Bool) -> Void) {
// check user votes collection to see if current message matches
guard let currentUser = Auth.auth().currentUser else {return false}
let userID = currentUser.uid
let docRef = Firestore.firestore().collection("users").document(userID).collection("upvotes").whereField("messageId", isEqualTo: message.messageId)
docRef.getDocuments { querySnapshot, error in
if let error = error {
print("Error getting documents: \(error)")
completion(false)
} else {
for document in querySnapshot!.documents {
print("\(document.documentID) => \(document.data())")
completion(true) /// Note that this will get called multiple times if you have more the one document!
}
}
}
}
Usage
didAlreadyVote(message: messageType) { didVote in
// didVote is the value returned
}

Swift closures and error handling

hello, i need some help with function and or possibly closures in that function. I want my function to check the firestore users collection for duplicate documents (usernames). If a duplicate is found i want to display a message, if a duplicate is not found, create a new user. i have folowing code:
func checkIfUserExists(username: String, completion: #escaping (Bool) -> Void) {
let docRef = db.collection("users").document(username)
docRef.getDocument { (document, error) in
if error != nil {
print(error)
} else {
if let document = document {
if document.exists {
completion(true)
} else {
completion(false)
}
}
}
}
}
and i call the function with:
if let username = textField.text, username.count > 8 { // Username needs to be more then 8 Char
checkIfUserExists(username: username) { (doesExist) in
if doesExist {
print("user exists")
} else {
print("new User can be created")
}
}
} else {
print("Username needs to be more then 8 char")
}
}
It works, but i have the feeling it is not good practice and i'm making detours. Is this the right way to do it ?
I think the way you're doing it now should work well, but another option to prevent you from having to do a read of the database before writing is to use security rules. For example, if this is the structure of your users collection...
users: [
username1: { // doc ID is the username
userid: abcFirebaseUserId, // a field for the uid of the owner of the username
//...etc
}
]
...then you can use the following rules:
match /users/{username} {
allow create: if request.auth.uid != null;
allow update, delete: if resource.data.userId = request.auth.uid;
}
This allows any authenticated user to create a new username, but only the owner of that username can update it or delete it. If you aren't allowing users to change their username, you wouldn't even have to worry about the second rule. Then, in the client, you go right to creating a username, like so:
func createUsername(username: String, completion: #escaping (String?) -> Void) {
guard let userId = Auth.auth().currentUser.uid else {
completion("no current user")
return
}
let docRef = db.collection("users").document(username)
docRef.setData(data:[userId: userId]) { error in
if let error = error {
completion(error.debugDescription)
} else {
completion(nil)
}
}
}
This would write the new username to the database and pass an error to the closure if there is one. If the username already exists, an insufficient permissions error would be present. When checking if the user exists, you could display the error or alert the user however you wanted.
createUsername(username: username) { err in
if let err = err {
print("user exists")
} else {
print("new User has been created")
}
}
Just a suggestion though. I think they way you're doing it now is fine, too!