Swift/Firestore: Completion doesn't get called - ios

I'm trying to create a "Profile" collection in Firestore in order to store more data on my users than just their email/name.
I'm stuck with creating this document and uploading the profile picture they choose (as an URL).
Here is the function called when they click on the "Register" button:
func register() {
Auth.auth().createUser(withEmail: self.email, password: self.pass) { (res, err) in
if err != nil {
self.error = err!.localizedDescription
self.alert.toggle()
return
}
// Success registering a new user
guard let userUID = res?.user.uid else { return }
uploadImageToFirestore(id: userUID, image: myImage) {
print("SUCCESS")
self.imageURL = downloadImageFromFirestore(id: userUID)
self.createUserDocument(id: userUID, imgURL: self.imageURL)
}
}
First step is uploading picture on Firebase Storage using the uploadImageToFirestore function and I tried using a completion handler to wait before calling the next 2 functions:
func uploadImageToFirestore(id: String, image: UIImage, completion: () -> Void) {
let storageRef = storage.reference().child("images/\(id)/image.jpg").putData(image.jpegData(compressionQuality: 0.35)!, metadata: nil) { (_, err) in
if err != nil {
print((err?.localizedDescription)!)
return
}
print("Success uploading picture to Firestore")
}
}
Second step is downloading the newly uploaded image on Firebase Storage to get the URL:
func downloadImageFromFirestore(id: String) -> String {
let storageRef = storage.reference().child("images/\(id)/image.jpg")
storageRef.downloadURL { url, error in
if error != nil {
print("DEBUG: \((error?.localizedDescription)!)")
return
}
print("Success downloading picture from Firestore")
self.imageURL = "\(url!)"
}
return self.imageURL
}
Third step is creating the Profile collection in Firestore with the ImageURL:
func createUserDocument(id: String, imgURL: String) {
db.collection("profiles").document(id).setData([
"name": name,
"surname": surname,
"email": email,
"shelter": isMember == true ? shelters[selectedShelter] : shelters[0],
"photoURL": imgURL,
"reputation": 0,
"uuid": id
])
{ err in
if let error = err {
print("Error ading document: \(error)")
} else {
print("New profile document created in Firestore")
}
}
}
THE PROBLEM
The problem I face is that in my "Register" function, the completion block of uploadImageToFirestore is never called, thus the function createUserDocument neither.
Is this the best way to achieve what I want (a.k.a. creating a profile document with the imageURL of the picture they just choose while registering)? Why is my completion block not called? (I don't see the "SUCCESS" printed in my console).
Thank you for your help!
Kind regards,
Jihaysse

You need to call the completion handler closure that you are passing to uploadImageToFirestore.
You should probably also pass the error, if any, so that you can handle it.
func uploadImageToFirestore(id: String, image: UIImage, completion: (Error?) -> Void) {
let storageRef = storage.reference().child("images/\(id)/image.jpg").putData(image.jpegData(compressionQuality: 0.35)!, metadata: nil) { (_, err) in
completion(err)
}
}

Related

IOS Firebase | Need to load document which could be in one of two collections

My database is currently organized into two collections: male_users & female_users. When the app is first launched AND the user is already logged in, I attempt to pull their usernode from the database. The problem I am facing is, at this time I don't know whether to search the MALE_COLLECTION or FEMALE_COLLECTION to find the user. What would be the proper way of working around this? Should I use user defaults to save the gender of the last user?
static func fetchUser(withUid uid: String, completion: #escaping (User) -> Void) {
COLLECTION_MALE_USERS.document(uid).getDocument { (snapshot, error) in
if let userNode = snapshot?.data() {
guard let user = User(with: userNode) else {
print("DEBUG: Failed to create user")
return
}
completion(user)
}
else {
COLLECTION_FEMALE_USERS.document(uid).getDocument { snapshot, error in
guard let userNode = snapshot?.data() else {
print("DEBUG: No user node found")
return
}
guard let user = User(with: userNode) else {
print("DEBUG: Failed to create user")
return
}
completion(user)
}
}
}
}
I would suggest doing something like Ahmed Shendy suggested but I'm not familiar with geofire.
You could use something like shown below or better yet, move the second fetch to a new function and call that new function after fetching for male users produces no results.
func fetchUser(uid: String, onSuccess: #escaping(_ user: User) -> Void, onError: #escaping(_ errorMessage: String) -> Void) {
let maleDocRef = COLLECTION_MALE_USERS.document(uid)
let femaleDocRef = COLLECTION_FEMALE_USERS.document(uid)
maleDocRef.getDocument { (document, error) in
if let error = error {
onError(error.localizedDescription)
return
}
if let document = document, document.exists {
print("male user exists")
guard let data = document.data() else { return }
let user = User(with: data)
onSuccess(user)
} else {
print("male user does no exists")
// BOF second fetch
femaleDocRef.getDocument { (document, error) in
if let error = error {
onError(error.localizedDescription)
return
}
if let document = document, document.exists {
print("female user exists")
guard let data = document.data() else { return }
let user = User(with: data)
onSuccess(user)
} else {
print("no user either male or female exist")
}
}
// EOF second fetch
}
}
}

Not sure where in code to place storageRef.downloadURL

After updating Firebase to Version 4 and correcting all 200 errors, I am left with 2 warnings which make my app crash. I looked up this error and tried the resolution with no success:
storageReference.downloadURLWithCompletion()
I must be doing it wrong:
func setUserInfo(_ user: User!, usersname: String, email: String, password: String, cell: String, data: Data!) {
// Create Path for User Image
let imagePath = "images/riders/\(user.uid)/Profile Pic/userPic.jpg"
// Create image Reference
let imageRef = rDataService.Instance.storageRef.child(imagePath)
// Create Metadata for the image
let metaData = StorageMetadata()
metaData.contentType = "image/jpeg"
// Save the user Image in the Firebase Storage File
imageRef.putData(data as Data, metadata: metaData) { (metaData, error) in
if error == nil {
let changeRequest = user.createProfileChangeRequest()
changeRequest.displayName = usersname
changeRequest.photoURL = metaData?.downloadURL()
changeRequest.commitChanges(completion: { (error) in
if error == nil {
self.saveUser(user, usersname: usersname, email: email, password: password, cell: cell)
} else {
print(error!.localizedDescription)
}
})
} else {
print(error!.localizedDescription)
}
}
}
The error is happening on this line:
changeRequest.photoURL = metaData?.downloadURL()
Edit
After adjustments, getting warning on this line:
if let profilepicMetaData = profilepicMetaData {
error: Value 'profilepicMetaData' was defined but never used; consider replacing with boolean test
App is still crashing:
// Save the user profile Image in the Firebase Storage File
imageRef.putData(data as Data, metadata: profilepicMetaData) { (profilepicMetaData, error) in
if let profilepicMetaData = profilepicMetaData {
imageRef.downloadURL(completion: { (url, error) in
guard let url = url else {
if let error = error {
print(error)
}
return
}
let changeRequest = user.createProfileChangeRequest()
changeRequest.displayName = usersname
changeRequest.photoURL = url
changeRequest.commitChanges(completion: { (error) in
if error == nil {
self.saveUser(user, usersname: usersname, email: email, password: password, year: year, makeAndModel: makeAndModel, cell: cell, plateNo: plateNo)
} else {
print(error!.localizedDescription)
}
})
})
} else {
print(error!.localizedDescription)
}
}
Crash!
You need to use the original storage reference object imageRef to obtain the download url. (please check the comments through the code):
imageRef.putData(data, metadata: profilepicMetaData) {
// use if let to unwrap the metadata returned to make sure the upload was successful. You can use an underscore to ignore the result
if let _ = $0 {
// start the async method downloadURL to fetch the url of the file uploaded
imageRef.downloadURL {
// unwrap the URL to make sure it is not nil
guard let url = $0 else {
// if the URL is nil unwrap the error, print it
if let error = $1 {
// you can present an alert with the error localised description
print(error)
}
return
}
// your createProfileChangeRequest code needs to be run after the download url method completes. If you place it after the closure it will be run before the async method finishes.
let changeRequest = user.createProfileChangeRequest()
changeRequest.displayName = usersname
changeRequest.photoURL = url
changeRequest.commitChanges {
// unwrap the error and print it
if let error = $0 {
// again you might present an alert with the error
print(error)
} else {
// user was updated successfully
self.saveUser(user, usersname: usersname, email: email, password: password, year: year, makeAndModel: makeAndModel, cell: cell, plateNo: plateNo)
}
}
}
} else if let error = $1 {
// present an alert with the error
print(error)
}
}

Enabling cache for AWS AppSync Client iOS Swift

I am using AWS AppSync for creating my iOS application. I want to leverage the offline mutation as well as query caching provided by AppSync. But when I am turning my internet off, I am not getting any response. Rather its showing an error as "The Internet connection appears to be offline.". This seems to be rather an Alamofire exception than an AppSync exception. This is because the query is not getting cached inside my device. Following is my code snippet to initialize the client.
do {
let appSyncClientConfig = try AWSAppSyncClientConfiguration.init(url: AWSConstants.APP_SYNC_ENDPOINT, serviceRegion: AWSConstants.AWS_REGION, userPoolsAuthProvider: MyCognitoUserPoolsAuthProvider())
AppSyncHelper.shared.appSyncClient = try AWSAppSyncClient(appSyncConfig: appSyncClientConfig)
AppSyncHelper.shared.appSyncClient?.apolloClient?.cacheKeyForObject = { $0["id"] }
} catch {
print("Error in initializing the AppSync Client")
print("Error: \(error)")
UserDefaults.standard.set(nil, forKey: DeviceConstants.ID_TOKEN)
}
I am caching the token in the UserDefaults at the time of fetching the session, and then whenever the AppSyncClient is called, it fetches the latest token by calling the getLatestAuthToken() method of my MyCognitoUserPoolsAuthProvider: AWSCognitoUserPoolsAuthProvider. This is returning the token stored in the UserDefaults -
// background thread - asynchronous
func getLatestAuthToken() -> String {
print("Inside getLatestAuthToken")
var token: String? = nil
if let tokenString = UserDefaults.standard.string(forKey: DeviceConstants.ID_TOKEN) {
token = tokenString
return token!
}
return token!
}
My query pattern is the following
public func getUserProfile(userId: String, success: #escaping (ProfileModel) -> Void, failure: #escaping (NSError) -> Void) {
let getQuery = GetUserProfileQuery(id: userId)
print("getQuery.id: \(getQuery.id)")
if appSyncClient != nil {
print("AppSyncClient is not nil")
appSyncClient?.fetch(query: getQuery, cachePolicy: CachePolicy.returnCacheDataElseFetch, queue: DispatchQueue.global(qos: .background), resultHandler: { (result, error) in
if error != nil {
failure(error! as NSError)
} else {
var profileModel = ProfileModel()
print("result: \(result)")
if let data = result?.data {
print("data: \(data)")
if let userProfile = data.snapshot["getUserProfile"] as? [String: Any?] {
profileModel = ProfileModel(id: UserDefaults.standard.string(forKey: DeviceConstants.USER_ID), username: userProfile["username"] as? String, mobileNumber: userProfile["mobileNumber"] as? String, name: userProfile["name"] as? String, gender: (userProfile["gender"] as? Gender).map { $0.rawValue }, dob: userProfile["dob"] as? String, profilePicUrl: userProfile["profilePicUrl"] as? String)
} else {
print("data snapshot is nil")
}
}
success(profileModel)
}
})
} else {
APPUtilites.displayErrorSnackbar(message: "Error in the user session. Please login again")
}
}
I have used all the 4 CachePolicy objects provided by AppSync, i.e,
CachePolicy.returnCacheDataElseFetch
CachePolicy.fetchIgnoringCacheData
CachePolicy.returnCacheDataDontFetch
CachePolicy.returnCacheDataAndFetch.
Can someone help me in implementing the cache properly for my iOS app so that I can do queries without the internet also?
Okay so I found the answer myself. The databaseUrl is an optional argument. It does not come in the suggestions when we are initializing the AWSAppSyncClientConfiguration object.
So the new way in which I initialized the client is the following
let databaseURL = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(AWSConstants.DATABASE_NAME, isDirectory: false)
do {
let appSyncClientConfig = try AWSAppSyncClientConfiguration.init(url: AWSConstants.APP_SYNC_ENDPOINT,
serviceRegion: AWSConstants.AWS_REGION,
userPoolsAuthProvider: MyCognitoUserPoolsAuthProvider(),
urlSessionConfiguration: URLSessionConfiguration.default,
databaseURL: databaseURL)
AppSyncHelper.shared.appSyncClient = try AWSAppSyncClient(appSyncConfig: appSyncClientConfig)
AppSyncHelper.shared.appSyncClient?.apolloClient?.cacheKeyForObject = { $0["id"] }
} catch {
print("Error in initializing the AppSync Client")
print("Error: \(error)")
}
Hope it helps.

Swift Error: NSInternalInconsistencyException, 'Upload attempting to execute on non main queue! Only on the main queue.'

I'm implementing a sign in with Twitter option on my app (with TwitterKit) but it keeps crashing on the function below - saveUserIntoFirebaseDatabase (when uploading the user data, image to Firebase database). I can't understand how OR why the function below needs to be in the main queue?
The user data is fetched and saved to the Firebase Auth section however it seems to crash after that when trying to save the user data to the Realtime Database & Storage?
fileprivate func saveUserIntoFirebaseDatabase() {
guard let uid = Auth.auth().currentUser?.uid,
let name = self.name,
let username = self.username,
let email = self.email,
let profileImage = profileImage,
let profileImageUploadData = UIImageJPEGRepresentation(profileImage, 0.3) else { Service.dismissHud(self.hud, text: "Error", detailText: "Failed to save user.", delay: 3); return }
let filename = UUID().uuidString
let storageRef = Storage.storage().reference().child("profile_images").child(filename)
storageRef.putData(profileImageUploadData, metadata: nil, completion: { (metadata, err) in
if let err = err {
Service.dismissHud(self.hud, text: "Error", detailText: "Failed to save user with error: \(err)", delay: 3);
return
}
// Firebase 5 Update: Must now retrieve downloadURL
storageRef.downloadURL(completion: { (downloadURL, err) in
guard let profileImageUrl = downloadURL?.absoluteString else { return }
print("Successfully uploaded profile image into Firebase storage with URL:")
let dictionaryValues = ["name": name,
"email": email,
"username": username,
"profileImageUrl": profileImageUrl]
let values = [uid : dictionaryValues]
Database.database().reference().child("users").updateChildValues(values, withCompletionBlock: { (err, ref) in
if let err = err {
Service.dismissHud(self.hud, text: "Error", detailText: "Failed to save user info with error: \(err)", delay: 3)
return
}
print("Successfully saved user info into Firebase database")
// after successfull save dismiss the welcome view controller
self.hud.dismiss(animated: true)
self.dismiss(animated: true, completion: nil)
})
})
})
}
The MainTabBarController I have setup has the following to see if the current user is logged in, if not they get re-directed to the Welcome Controller. Could this be the cause?
fileprivate func checkLoggedInUserStatus() {
if Auth.auth().currentUser == nil {
DispatchQueue.main.async {
let welcomeController = WelcomeController()
let welcomeNavigationController = UINavigationController(rootViewController: welcomeController)
self.present(welcomeNavigationController, animated: false, completion: nil)
return
}
}
}
I think you're trying to dismiss the viewcontroller on a background thread.

DispatchGroup will only exit when method is called twice

I'm trying to sign up users with Firebase auth. When a user signs up, I'd like them to be added to my Users collection in Firestore as well as the Users authorization section.
The createUser(withEmail: ...) method works every time. However, my db.collection("users").document(user.id).setData([..] method will only be called if I press the sign up button twice, and at that point the createUser(withEmail ...) method gets called again. Here's the relevant code
SignupViewController.swift
#IBAction func signupButtonTapped(_ sender: UIButton) {
// user: User() defined here
usersHelper.signup(user: user, password: password) { result in
// This closure is only executed on the second press
guard let user = result as? Firebase.User else {
let error = result as? Error
self.handleSignupError(error!)
return
}
self.performSegue(withIdentifier: "ShowGroupsFromSignupSegue", sender: self)
}
}
UsersHelper.Swift
func signup(user: User, password: String, completion: #escaping (_ result: Any?) -> Void) {
let userDispatchGroup = DispatchGroup()
var signupError: Error? = nil
var dbError: Error? = nil
var firebaseUser: Firebase.User? = nil
userDispatchGroup.enter()
usersDataModel.signupUser(user: user, password: password) { result in
// Completion handler
if result as? Error != nil {
signupError = result as? Error
} else {
// Got the user
firebaseUser = result as? Firebase.User
}
userDispatchGroup.leave()
}
userDispatchGroup.enter()
usersDataModel.create(user: user) { err in
// This will only execute if signUp is called twice
if let result = err as? Error {
print("Error msg: \(result.localizedDescription)")
dbError = result
}
print("!Created db user")
userDispatchGroup.leave()
}
userDispatchGroup.notify(queue: .main) {
print("!dispatch group completed successfully")
if (signupError == nil && dbError == nil) {
completion(firebaseUser)
} else {
signupError != nil ? completion(signupError) : completion(dbError)
}
}
}
UsersDataModel.swift
func signupUser(user: User, password: String, _ completion: #escaping (_ err: Any? ) -> Void) {
// Create user in Auth & create DB entry
Auth.auth().createUser(withEmail: user.email, password: password) { (authResult, err) in
if let err = err {
print("Error creating user \(err)")
completion(err)
} else {
print("User signed up successfully")
completion(authResult) // completion called with User
}
}
}
func create(user: User, _ completion: #escaping (_ result: Any?) -> Void) {
// userData dictionary created here
db.collection("users").document(user.ID).setData(userData) { err in
if let err = err {
print("There was an error creating the user \(err)")
completion(err)
} else {
print("!User created in db successfully!")
completion(nil)
}
}
}
Any help is greatly appreciated! Thank you all in advance
I've resolved the error. I ended up nesting the second network call in order to:
Get the uid from the firestore who was authenticated
Not break firestore rules about writing to the database w/o an authorized uid
My UsersHelper.swift file now looks like
func signup(user: User, password: String, completion: #escaping (_ result: Any?) -> Void) {
let userDispatchGroup = DispatchGroup()
var signupError: Error? = nil
var dbError: Error? = nil
var firebaseUser: Firebase.User? = nil
userDispatchGroup.enter()
usersDataModel.signupUser(user: user, password: password) { result in
// Completion handler
if result as? Error != nil {
// there was an error?
print("Error: \(result)")
signupError = result as? Error
} else {
// Got the user
firebaseUser = result as? Firebase.User
// Create user entry in DB
user.ID = firebaseUser!.uid
self.usersDataModel.create(user: user) { err in
// Completion handler
if let err = err as? Error {
dbError = err
}
userDispatchGroup.leave()
print("Done")
}
}
}
userDispatchGroup.notify(queue: .main) {
print("!dispatch group completed successfully")
if (signupError == nil && dbError == nil) {
completion(firebaseUser)
} else {
signupError != nil ? completion(signupError) : completion(dbError)
}
}
}

Resources