Omg… You are brilliant! Thank you so much Chris!
No worries. Now you have to get the LoginViewController sorted though there is less to do there.
Already done,
Hi @Chris_Parker,
I am trying to fetch the user’s name but nothing is popping up. It’s just blank. I’m not getting any errors which probably means my pathing is wrong, I’m guessing. What do you thinks? This is based on your above code about user data.
fetchUser()
func fetchUser() {
let userUID = Auth.auth().currentUser?.uid
Firestore.firestore().collection("users").document(userUID!).getDocument { snapshot, error in
if error != nil {
// ERROR
}
else {
let firstName = snapshot?.get("firstName")
let cellLbl = UILabel(frame: CGRect(x: 110, y: cell.frame.height/2-15, width: 250, height: 30))
cell.addSubview(cellLbl)
cellLbl.text = firstName as? String
cellLbl.font=UIFont.systemFont(ofSize: 17)
cellLbl.textColor = .white
}
}
}
The first thing to note that in the code that saves the user, the documentID is not the currentUser.uid. It is an auto generated document ID which is why your fetchUser code is not working.
The user record has the uid stored within it as a field so code to retrieve the correct record can be done using a query with a whereField clause.
For example:
func fetchUser() {
let db = Firestore.firestore()
let users = db.collection("users")
let userUID = Auth.auth().currentUser?.uid
let query = users.whereField("uid", isEqualTo: userUID!)
query.getDocuments { snapshot, error in
guard error == nil else { return}
guard let snapshot = snapshot else {
return
}
for doc in snapshot.documents {
let firstName = doc["firstName"] as! String
//let cellLbl = UILabel(frame: CGRect(x: 110, y: cell.frame.height/2-15, width: 250, height: 30))
//cell.addSubview(cellLbl)
self.cellLbl.text = firstName
self.cellLbl.font = UIFont.systemFont(ofSize: 17)
//self.cellLbl.textColor = .white
}
}
}
In my test project I had to add a label to the ViewController I was using and named it so that it worked to some degree but I also I commented out some of the code you had since it did not apply.
Just uncomment those lines and I assume it will work.
You could go down the path of using the currentUser.uid as the documentID for that user record but I personally don’t favour that approach. The query in fetchUser is pretty fast even though it is searching through all the users in the collection to find the match so I don’t see any issues with lags on data retrieval speed. The point being that currentUser.uid is unique so it will find that single record rapidly.
I see! Brilliant! Thanks again. It works great!
Hey @Chris_Parker,
Is it possible to add an existing document? For example the user that has signed up now has an option to add a Nickname. Adding to a existing document is something I haven’t done yet in swift. How might I do this?
Yes it is. How are you setting the documentID for each user that is added to the users collection? Are you letting that documentID be generated automatically or are you setting it to the currentUser.uid?
Current user. I was trying to implement it but it wasn’t working out. A previous one that wrote didn’t target the document but created a new document, which isn’t what I’m trying to do.
@IBAction func signUpTapped(_ sender: Any) {
errorLabel.alpha = 0
errorLabel.text = ""
saveUserData()
}
// Save User Data
func saveUserData() {
// 8.1 Create cleaned version of the data
let nickname = nicknameTextField.text!.trimmingCharacters(in: .whitespacesAndNewlines)
let user = Auth.auth().currentUser
// 11. User created successfully, now store the fist name and last name
let db = Firestore.firestore()
db.collection("users").addDocument(data: ["nickname": nickname]
) { (error) in
if error != nil {
// 12. Show error message
// Unwrap error because it is Optional.
if let error = error {
// self.showError(message: "Successfully saved user data.")
// 13. Transition to the home screen
self.transitionToHome()
}
}
}
}
func transitionToHome() {
let profileVC = UIStoryboard(name: "Main", bundle: nil)
let controller = profileVC.instantiateViewController(identifier: "profile") as! ProfileViewController
controller.modalPresentationStyle = .fullScreen
//present(controller , animated: true)
self.view.window?.rootViewController = UINavigationController(rootViewController: controller)
self.view.window?.makeKeyAndVisible()
}
}
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.