Right now my app is crashing when a collection already exists in Firebase Firestore. I want to catch this error when it happens, but my current implementation doesn't catch anything as the addSnapshotListener() method does not throw any error.
Current Code
let db = Firestore.firestore()
do {
try db.collection(chatName).addSnapshotListener { (Query, Error) in
if Error != nil || Query?.documents != nil {
let alert = UIAlertController(title: "Chat Name Already Exists", message: "This chat name already exists, try again with a different name", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "Okay", style: .default, handler: { (UIAlertAction) in
alert.dismiss(animated: true, completion: nil)}))
AllChatsViewController().present(alert, animated: true)
completion()
}
else {
self.addChatToProfile(withName: chatName) {
completion()
}
}
}
}
catch {
let alert = UIAlertController(title: "Chat Name Already Exists", message: "This chat name already exists, try again with a different name", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "Okay", style: .default, handler: { (UIAlertAction) in
alert.dismiss(animated: true, completion: nil)}))
AllChatsViewController().present(alert, animated: true)
completion()
}
Error After App Crashes
Thread 1: "Invalid collection reference. Collection references must have an odd number of segments, but has 0"
How can I catch this error so I can display an UIAlertController with the error?
I would use a different approach.
To test if a collection exists, read that collection by name and determine if there are any documents via snapshot.count equals 0
The gotcha here is that a collection could have a large amount of data and there's no reason to read all of that in or attach a listener so we need to use a known field within that collection to limit the results.
I would suggest a function with a closure that returns true if the collection exists, false if not and then take action based on that result.
You'll need the name of the collection you want to test and then the name of a known field within that collection to query for to limit the results.
The field name is important in that if the collection has 1M documents, you don't want to read them all in - you just want to read one and .orderBy with a limit will do that.
So here's a calling function
func checkCollection() {
self.doesCollectionExist(collectionName: "test_collection", fieldName: "msg", completion: { isEmpty in
if isEmpty == true {
print("collection does not exist")
} else {
print("collection found!")
}
})
}
and then the function that checks to see if the collection exists by reading one document and returns false if not, true if it does.
func doesCollectionExist(collectionName: String, fieldName: String, completion: #escaping ( (Bool) -> Void ) ) {
let ref = self.db.collection(collectionName)
let query = ref.order(by: fieldName).limit(toLast: 1)
query.getDocuments(completion: { snapshot, error in
if let err = error {
print(err.localizedDescription)
return
}
if snapshot!.count == 0 {
completion(true)
} else {
completion(false)
}
})
}
That error doesn't have anything to do with a collection not existing The error suggests that chatName is an empty string, which is an invalid parameter. Instead of catching an error, you should instead first validate that chatName is a valid collection name string before sending it to the Firestore API.
If you query a collection that doesn't exist, you won't get that error message at all. Instead, you will simply get no documents in the result set.
you will make something like this:
Firestore.firestore().collection(chatName).addSnapshotListener { (query, error) in
if let error = error {
//in this part you have the error, do something like present alert with error or something you want
print(error)
}
// in this part the success
Related
I am currently trying to get what type of error is thrown when a user attempts to sign in/ registers but my switch case is not working because I don't know what enum I am supposed to compare to. Once the error can be determined the app displays a UIAlertController explaining the error. If I print(err.code) I just get an Int back. Can anyone steer me in the right direction? I couldn't find any docs on how to handle it.
func signIn(username: String, password: String) {
Amplify.Auth.signIn(username: username, password: password) { result in
switch result {
case .success:
print("Sign in succeeded")
//Go to root vc
case .failure(let error):
print("Sign in failed \(error)")
if let err = error as NSError?{
switch err.code {
case AWSCognitoIdentityProviderErrorType.unknown.rawValue:
self.presentAlert(errorTitle: "Unkown Error", errorMessage: "An unknown error has occured", buttonText: "Ok")
print("Unkown error")
case AWSCognitoIdentityProviderErrorType.invalidPassword.rawValue:
self.presentAlert(errorTitle: "Invalid Password", errorMessage: "You have entered an invalid password", buttonText: "Try Again")
case AWSCognitoIdentityProviderErrorType.tooManyFailedAttempts.rawValue:
self.presentAlert(errorTitle: "Excedded login trys", errorMessage: "You attempted to login too many times", buttonText: "Try Again Later")
case AWSCognitoIdentityProviderErrorType.userNotFound.rawValue:
self.presentAlert(errorTitle: "Unknown Credentials", errorMessage: "No user exists with the credentials you entered.", buttonText: "Try Again")
default:
break
}
}
}
}
}
The type of error thrown by the Amplify Authentication library is an AuthError type, which contains a more helpful embedded AWSCognitoAuthError type. The AWS documentation is confusing and misleading because AWS provides two SDKs at this time: the AWS Mobile SDK and the newer, dumber Amplify Libraries.
It looks like you are using the Amplify Libraries. If you break your code in the Xcode debugger on the .failure case, the debugger variable display shows error as Amplify.AuthError which requires a PhD in AWS to make sense of. But for purposes of illumination, if you slug this in under the .failure case
case .failure(let error):
if let actualError = error.underlyingError as NSError? {
print("Cast to nserror:", actualError)
}
you will get Cast to nserror: AWSCognitoAuthPlugin.AWSCognitoAuthError.xxx where the xxx might be usernameExists or some other useful error. It turns out AWSCognitoAuthError is a straightforward enum which you can peruse by typing AWSCognitoAuthError at some random spot in your source, then right-clicking and choosing Jump to Definition in the Xcode popup menu.
I prefer to keep all the voodoo Amplify knowledge isolated, so I created a User class to interact with AWS Authentication services. I include the error handling portions here in hopes it saves you some grief.
// At the top of the file, outside the class:
import Amplify
import AWSCognitoAuthPlugin
import AWSPluginsCore
import Foundation
// This typealias allows other modules to interpret error responses
// from the AWSCognitoAuthError enum, such as ".usernameExists" without
// needing to import Amplify everywhere.
typealias AWSError = AWSCognitoAuthError
// Amplify Auth methods return AuthError type errors, which are complex multilayer
// enums that require complex deciphering if you want to extract a simple error
// to guide the user.
//
// This custom Error type allows callers from other modules to receive a simple
// explanatory "message" string, which is pulled from the .errorDescription
// property of an Amplify AuthError. The "error" optional is the returned
// .underlyingError property cast to an AWSCognitoAuthError type, which is an
// enum of all the possible problems interacting with Amplify Auth such as
// .usernameExists, .userNotConfirmed, .codeMismatch, or .codeExpired.
struct UserError: Error {
let message: String
let error: AWSError?
init(message: String?, error: Error?) {
self.message = message ?? "Unknown error"
self.error = error as? AWSError
}
}
// Example of usage, within the class. In my case the class signIn()
// function includes an onCompletion callback parameter to allow the
// caller to inform the user and change the UI according to the results.
func signIn(username: String, password: String,
onCompletion: #escaping (Result<Bool, UserError>) -> Void) {
_ = Amplify.Auth.signIn(username: username, password: password) { result in
switch result {
case .success(let signInResult):
onCompletion(.success(signInResult.isSignedIn))
case .failure(let authError):
let awsError = authError.underlyingError as? AWSError ?? AWSError.userNotFound
let userError = UserError(message: authError.errorDescription,
error: awsError)
onCompletion(.failure(userError))
}
}
}
In your view controller, here is how you might process the response from the example signIn() User class method above:
// Inside e.g. an #IBAction function
User.sharedUser.signIn(email, password: password, onCompletion: { result in
switch result {
case .success:
// Go to root View Controller
case .failure(let userError):
if let authError = userError.error {
if (authError == .userNotConfirmed) {
DispatchQueue.main.async {
self.performSegue(withIdentifier: "ShowConfirm", sender: nil)
}
return
}
}
let alert = UIAlertController(title: userError.message, message: nil, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
DispatchQueue.main.async {
self.present(alert, animated: true, completion: nil)
}
}
})
I deleted an entry in the Firestore and also checked it manually to confirm that. However, as long as I do not close the application, I can send a request to fetch the data and I still get the result. This should not be the case.
If you imagine having a shared photo with some textual information and you delete those information, this would mean, other users can still see the textual information (fetched from the Firestore) but not the image anymore (store in Firestorage).
I want to display a message on the UI, something like "The content does not exist anymore".
How I can achieve that? I used the following approach so far but it does not work at the moment:
public func checkIfChallengeObjectExists(completionHandler:#escaping(Bool)->(), challengeId:String) {
CHALLENGE_COLLECTION?.document(challengeId).getDocument(completion: { (querySnapshot, error) in
if (error != nil) {
print(error?.localizedDescription as Any)
}
if (querySnapshot?.documentID == "" || querySnapshot!.metadata.isFromCache) {
completionHandler(false)
}
else {
completionHandler(true)
}
})
}
Any solutions?
Non-existent documents will still return document snapshots, but they will be empty. Therefore, you must check the contents of the snapshot for the document, not the snapshot itself. Also, you should handle errors and the overall flow of the return better.
public func checkIfChallengeObjectExists(completionHandler:#escaping(Bool)->(), challengeId:String) {
CHALLENGE_COLLECTION?.document(challengeId).getDocument(completion: { (querySnapshot, error) in
if let doc = querySnapshot,
doc.exists {
completionHandler(true) // only one possible true condition
} else {
if let error = error {
print(error.localizedDescription)
}
completionHandler(false) // all else false
}
})
}
As a side note, I recommend reordering the parameters of the function to make it easier to read when called (conventionally, the completion handler comes last) and giving the boolean argument a name so it's easier to read when referencing (sometime later or by other developers).
public func verifyChallengeObject(ID: String, _ completion: #escaping (_ exists: Bool) -> Void) {
...
}
verifyChallengeObject(ID: "abc123", { (exists) in
if exists {
...
} else {
...
}
})
I have implemented Auth method for the Firestore database, but when the user tries to register with the same email, the app crash. I'd like to implement a function to check if the email already exists (if it does, fire UIAlert, otherwise if it doesn't, create a new user).
I have so far:
Auth.auth().createUser(withEmail: email, password: password) { (Result, err) in
let db = Firestore.firestore()
let docRef = db.collection("email users").document("email")
docRef.getDocument { (document, error) in
if let document = document, document.exists {
let emailAlreadyInUseAlert = UIAlertController(title: "Error", message: "Email already registered", preferredStyle: .alert)
emailAlreadyInUseAlert.addAction(UIAlertAction(title: "OK", style: .cancel, handler: nil))
self.present(emailAlreadyInUseAlert, animated: true, completion: nil)
return
} else {
let db = Firestore.firestore()
db.collection("email users").addDocument(data: [
"firstName": firstName,
"lastName": lastName,
"email": email,
"created": Timestamp(date: Date()),
"uid": Result!.user.uid
])
}
self.transitionToHome()
}
}
}
}
func transitionToHome() {
let homeViewController = storyboard?.instantiateViewController(identifier: "HomeViewController") as? HomeViewController
view.window?.rootViewController = homeViewController
view.window?.makeKeyAndVisible()
}
}
At this code the UIAlert doesn't fire, and have an error at : "uid": Result!.user.uid - Thread 1: Fatal error: Unexpectedly found nil while unwrapping an Optional value. When creating new user with unique email, it works as it should, the user is created.
If I change if let document = document, document.exists to if error !=nil, I get the UIAlert when the email already exist and also if it doesnt exist, the code of creating user doesnt execute.
Even tried to implement addsnapshotlistener, no luck.
Any suggestions? Thank you
Regarding error: it's a common practice to return success code as error code instead of setting error to nil, and google docs seems mention it as well.
The other issue is because you are forcibly unwrapping items that can legitimately be nil.
Instead, use guard to isolate any invalid cases and exit:
guard error == nil || case FirestoreErrorCode.OK = error else {
// got error; process it and
return
}
guard let result = result else {
// got no error, but no result either
// fail and
return
}
//if you are here, it means you've got no error and `result` is not nil.
Also notice that result should not be capitalized in callback:
Auth.auth().createUser(withEmail: email, password: password) { (result, err) in ...
You may not need a custom function to check if an email already exists as that's a default error Firebase Auth will catch and allow you to handle when a user is created.
For example, this code will catch situations where the user is attempting to use an email that already exists.
func createUser() {
let email = "test#thing.com"
Auth.auth().createUser(withEmail: email, password: "password", completion: { authResult, error in
if let x = error {
let err = x as NSError
switch err.code {
case AuthErrorCode.wrongPassword.rawValue:
print("wrong password")
case AuthErrorCode.invalidEmail.rawValue:
print("invalid email")
case AuthErrorCode.accountExistsWithDifferentCredential.rawValue:
print("accountExistsWithDifferentCredential")
case AuthErrorCode.emailAlreadyInUse.rawValue:
print("email already in use")
default:
print("unknown error: \(err.localizedDescription)")
}
return
}
let x = authResult?.user.uid
print("successfully created user: \(x)")
})
}
There's a number of Authentication Error Codes so you can handle a wide variety of errors without any special error handling.
And the AuthErrorCode API has some more useful information which is demonstrated in the answer code.
I created a basic Google Places app that lets users check-in to a location. When a user tries to check in, I loop through the list of likelihood places to verify that the user is actually at the location in the app. However, when I try to escape the loop after confirming the location is correct, my function still ends up going to my "else" situation (an error message that asks the user to please check in to the correct location).
The following function gets called in viewWillAppear:
func checkIn(handleComplete:#escaping (()->())){
guard let currentUserID = User.current?.key else {return}
// Specify the place data types to return.
let fields: GMSPlaceField = GMSPlaceField(rawValue: UInt(GMSPlaceField.name.rawValue) |
UInt(GMSPlaceField.placeID.rawValue))!
placesClient.findPlaceLikelihoodsFromCurrentLocation(withPlaceFields: fields, callback: {
(placeLikelihoodList: Array<GMSPlaceLikelihood>?, error: Error?) in
if let error = error {
print("An error occurred: \(error.localizedDescription)")
return
}
if let placeLikelihoodList = placeLikelihoodList {
for likelihood in placeLikelihoodList {
let place = likelihood.place
if likelihood.likelihood >= 0.75 && place.placeID! == self.hangoutID {
let place = likelihood.place
print("Current Place name \(String(describing: place.name!)) at likelihood \(likelihood.likelihood)")
print("Current PlaceID \(String(describing: place.placeID!))")
self.delta = 0.0
// update checkin
DispatchQueue.main.async {
let hangoutRef = self.db.collection("users").document(currentUserID).collection("hangout").document(self.hangoutID).updateData([
"lastCheckin": Date()
]) { err in
if let err = err {
print("Error updating document: \(err)")
} else {
print("Document successfully updated")
}
}
}
handleComplete()
}
}
self.presentDismissableAlert(title: "", message: "Please check in to the hangout to join this chat", button: "OK", dismissed: { (UIAlertAction) in
self.performSegue(withIdentifier: "unwindSegueToChats", sender: self)
})
}
})
}
If the correct conditions are met, the code will land on the handleComplete() line but then it will still execute the dismissableAlert underneath and segue the user out of the room. How can I fix the flow so that the app will cycle through the list of likely Places and stop the function on handleComplete if the correct condition is met, or else then proceed to the error message if the correct conditions are not met (user is not at the correct Place)?
Thanks
Beginner coder here. I have a question with Xcode. I am developing a horoscope app and have a question on linking my If Else Statements with picker input values.
The logic in the app currently is, User selects date listed, hits button, and an alert message pops up containing specific copy related to their input. How would I get my if else statements to print in the alert message?
TLDR; Need help with linking function print value with alert window message
Here is a sample of how it currently looks, I am also getting an error of [Cannot convert value of type '()' to expected argument type 'String?'] in line 3 here. Thank you in advance!
let message = signselection()
let alert = UIAlertController (title: "Your Astrological Sign is...", message:message, preferredStyle: .alert)
let okAction = UIAlertAction(title: "OK", style: .default) {(action) -> Void in}
func signselection () {
if (curMonth == 0) {
if(curDay! < 20) {
print("Capricorn Message")
} else {
print("Aquarius Message")
}
}
if (curMonth == 1){
if(curDay! < 19) {
print ("Aquarius Message")
} else {
print ("Pisces Message")
} }
if (curMonth == 2){
if(curDay! < 20) {
print ("Pisces Message")
} else {
print ("Aries Message")
} }
The signselection() method only prints the message, but it should return a string. That's why you're getting the error saying that it can't cast void, "()", to String. The function declaration should be:
func signSelection() -> String
and in stead of print("Message") you should
return "Message" //where "Message" is each message you're currently printing
You should also add the okAction to there alert with:
alert.addAction(okAction)