Value of type 'StorageMetadata' has no member 'downloadURL'

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.

1 Like

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
                                
                            }
                        }
                    }

@coder3000

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.

1 Like

I see! Brilliant! Thanks again. It works great!

1 Like

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?

@coder3000

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()

    }
    
 }

@coder3000

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? :thinking:

Foreach user, if the nickname equals nickname… Error. “Nickame already exists”?

@coder3000

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.

1 Like

@coder3000

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?

1 Like

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.

@coder3000

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.

1 Like

I have the error popping up. What is your suggestion: Value of type ‘NickName’ has no member ‘showError’