So what you are saying is that for every user that you save in your users
collection, you want to have the documentID to be the currentUser.uid. Is that correct?
Yes sir. Each nickname that is submitted should be specific to the user.
Are you expecting the nickname to be unique across all users?
yes sir.
That means you are going to have to loop through all the users and make sure that the nickname does not already exist.
That’s interesting. A foreach loop?
Foreach user, if the nickname equals nickname… Error. “Nickame already exists”?
It’s not that simple. You have to select all the documents from the users
collection in a snapshot and loop through all the snapshot.documents to see if the nickname already exists.
Firebase is not simple. It’s very awkward at times and often requires a lot of careful coding.
Something else to consider is that if you are opting to allow the user to add a nickname later on, the search through all the user documents needs to take into account the property may not exist for some records.
I think that makes it a bit awkward so my suggestion is that when you finally get your App finished, the user record needs to have all properties created at the time the user signs up even though they may be blank.
When you search through all the user records to determine if a nickname has already been used, you would ignore fields that are blank.
Do you see what I am getting at?
Yes. So I’ll add the fields that I need even though I don’t present it at the beginning of the SignUp process. This way the user can access them later since it is optional. I believe the code that was writing earlier was querying a field that wasn’t there. I will try that approach.
I’ve been tinkering with code to save a nickname when the user Logs In (ie, already a user) and it works nicely.
Essentially I have this function:
func addNickname(nickname: String) {
let cleanedNickname = nickname.trimmingCharacters(in: .whitespacesAndNewlines)
// Check if nickname is unique
isNicknameUnique(nickname: cleanedNickname) { isUnique in
if isUnique {
let db = Firestore.firestore()
// Where users document ID is set to currentUser.uid
// let user = db.collection("users").document("\(Auth.auth().currentUser?.uid ?? ""))")
// user.setData(["nickname": cleanedNickname], merge: true)
// OR
// Where users document ID is automatically created.
let users = db.collection("users")
let query = users.whereField("uid", in: [Auth.auth().currentUser?.uid ?? ""])
query.getDocuments { snapshot, error in
if let error = error {
self.showError(message: error.localizedDescription)
} else if let snapshot = snapshot {
for doc in snapshot.documents {
let user = db.collection("users").document("\(doc.documentID)")
user.setData(["nickname": cleanedNickname], merge: true)
self.showError(message: "Nickname saved.")
}
}
}
} else {
self.showError(message: "Nickname is already in use.")
}
}
}
which calls this function to test that the nickname supplied is unique.
func isNicknameUnique(nickname: String, completion: @escaping (Bool) -> Void) {
let db = Firestore.firestore()
// Set flag to true initially
var isUnique = true
let currentUserId = Auth.auth().currentUser?.uid ?? ""
let users = db.collection("users")
users.getDocuments { snapshot, error in
guard error == nil else {
self.showError(message: "Error retrieving records. Try again later.")
return
}
if let snapshot = snapshot {
for doc in snapshot.documents {
if let thisNickname = doc["nickname"] as? String, currentUserId != doc["uid"] as? String {
if thisNickname == nickname {
// Set flag to false
isUnique = false
break
}
}
}
}
completion(isUnique)
}
}
and is called from within the loginTapped Button code after login has been successful.
@IBAction func loginTapped(_ sender: Any) {
// Clear and hide the errorlabel
errorLabel.alpha = 0
errorLabel.text = ""
loginUser { success in
if success {
self.getProfileImage { returnedImage in
if let image = returnedImage {
self.logoImageView.image = image
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
self.transitionToHome()
}
}
}
if let nickname = self.nicknameTextField.text, self.nicknameTextField.text != "" {
self.addNickname(nickname: nickname)
}
}
}
}
The only thing is that the way I’ve written it, you only get one shot at setting the nickname on each login but the concept works.
Ideally you would probably add the nickname somewhere else in your App (say, in a Profile settings screen), by presenting a textField and a “Save Nickname” button so that you could continue to give the user a chance to figure out a nickname that is Unique and eventually get an acknowledgement that the nickname was saved.
I have the error popping up. What is your suggestion: Value of type ‘NickName’ has no member ‘showError’
Copy the showError function that is in the SignUpViewController and paste that in the current View Controller.
Oh ok. So all 3 are meant to LoginViewController? I have put it in a NicknameViewController, thinking that the user could set it up there. I miss understood. Also I am assuming that I need to add and extra field for the Nickname to be entered along side the email and password, correct? I just added the code to LoginViewController, but still has that error:
import UIKit
import FirebaseAuth
import FirebaseCore
import AVFoundation
import FirebaseFirestore
class LoginViewController: UIViewController {
@IBOutlet var emailTextField: UITextField!
@IBOutlet var passwordTextField: UITextField!
@IBOutlet var loginButton: UIButton!
@IBOutlet var errorLabel: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
setUpElements()
}
func setUpElements() {
// Hide error label
errorLabel.alpha = 0
// Style the elements
Utilities.styleTextField(emailTextField)
Utilities.styleTextField(passwordTextField)
Utilities.styleFilledButton(loginButton)
}
// 7. There's something wrong with the fields, show error message
func showError(message:String) {
errorLabel.text = message
errorLabel.alpha = 1
}
@IBAction func loginTapped(_ sender: Any) {
// 1. Validate Textfields
// 3. Create Cleaned verson of the Textfield
let email = emailTextField.text!.trimmingCharacters(in: .whitespacesAndNewlines)
let password = passwordTextField.text!.trimmingCharacters(in: .whitespacesAndNewlines)
// 2. Signing in user
Auth.auth().signIn(withEmail: email, password: password) { (result, error) in
if error != nil {
// 4. Couldn't sign in
self.errorLabel.text = error!.localizedDescription
self.errorLabel.alpha = 1
}
else {
loginUser { success in
if success {
self.getProfileImage { returnedImage in
if let image = returnedImage {
self.logoImageView.image = image
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
self.transitionToHome()
}
}
}
if let nickname = self.nicknameTextField.text, self.nicknameTextField.text != "" {
self.addNickname(nickname: nickname)
}
}
}
// 5. Transition to homescreen
let profileVC = UIStoryboard(name: "Main", bundle: nil)
let controller = profileVC.instantiateViewController(identifier: "tasks") as! TasksViewController
controller.modalPresentationStyle = .fullScreen
//present(controller , animated: true)
self.view.window?.rootViewController = UINavigationController(rootViewController: controller)
self.view.window?.makeKeyAndVisible()
}
}
}
func addNickname(nickname: String) {
let cleanedNickname = nickname.trimmingCharacters(in: .whitespacesAndNewlines)
// Check if nickname is unique
isNicknameUnique(nickname: cleanedNickname) { isUnique in
if isUnique {
let db = Firestore.firestore()
// Where users document ID is set to currentUser.uid
// let user = db.collection("users").document("\(Auth.auth().currentUser?.uid ?? ""))")
// user.setData(["nickname": cleanedNickname], merge: true)
// OR
// Where users document ID is automatically created.
let users = db.collection("users")
let query = users.whereField("uid", in: [Auth.auth().currentUser?.uid ?? ""])
query.getDocuments { snapshot, error in
if let error = error {
self.showError(message: error.localizedDescription)
} else if let snapshot = snapshot {
for doc in snapshot.documents {
let user = db.collection("users").document("\(doc.documentID)")
user.setData(["nickname": cleanedNickname], merge: true)
self.showError(message: "Nickname saved.")
}
}
}
} else {
self.showError(message: "Nickname is already in use.")
}
}
}
func isNicknameUnique(nickname: String, completion: @escaping (Bool) -> Void) {
let db = Firestore.firestore()
// Set flag to true initially
var isUnique = true
let currentUserId = Auth.auth().currentUser?.uid ?? ""
let users = db.collection("users")
users.getDocuments { snapshot, error in
guard error == nil else {
self.showError(message: "Error retrieving records. Try again later.")
return
}
if let snapshot = snapshot {
for doc in snapshot.documents {
if let thisNickname = doc["nickname"] as? String, currentUserId != doc["uid"] as? String {
if thisNickname == nickname {
// Set flag to false
isUnique = false
break
}
}
}
}
completion(isUnique)
}
}
}
In my case I put the code in the LoginViewController just for the purposes of testing that the code works.
If you had intended to have a NicknameViewController then that’s fine. Do all the nickname stuff in there and leave the LoginViewController just for the purposes of enabling the user to log in.
Perfect. Also… It seem as though the code wants to display the user’s profile pic …the part where getProfileImage is located. Correct? And, The only error left is: Cannot find ‘loginUser’ in scope. What am I missing?