I've done a lot of research through Firebase documentation, Swift/Xcode documentation, and here at Stack Overflow. Unfortunately, I have not been able to find a coherent and working solution to this problem--and it seems like this problem has been attempted many times in many different ways.
I'm not a professional coder. Needless to say, I'm figuring this out and teaching my children as we go along with this as a coding project to help them learn.
End goal: Allow swift/xcode application to create a user, but only if that user's userName is not already taken.
Relevant Stack Overflow discussions:
- Cloud Firestore: Enforcing Unique User Names
- Firestore Security Rule to Check if Character Username Already Exists
- Firestore Unique Index or Unique Constraint
I will attach my current Swift code and I will update it as I make progress on this issue. The current code is for an Xcode view controller for a signup process. This code creates two different collections in the firestore database: "users"--which contains the user's profile information, and "usernames"--which is essentially an index of all usernames.
This index (collection: usernames) has a document named after each username. The idea is to create a firebase rule that will not allow a user to create an account in which username = (name of document in collection: usernames).
Updates
It seems like there may be a couple of ways to accomplish the end goal. One is organically in the swift code, but that may have security issues as the code could potentially be bypassed by a nefarious actor. The other way is through rules in the firebase database, but I'm not as fluent in doing this (I'll do more research and report back). Perhaps the best course of action would be to do both.
This is the current code.
Version 1--the initial upload of code with the stated problem.
// SignUpViewController.swift
// Copyright © 2020 by Mix. All rights reserved.
// Version 1.
import UIKit
import Firebase
import FirebaseAuth
import FirebaseFirestore
class SignUpViewController: UIViewController, UITextFieldDelegate, UIPickerViewDelegate, UIPickerViewDataSource {
@IBOutlet weak var signUpButton: UIButton!
@IBOutlet weak var errorLabel: UILabel!
@IBOutlet weak var userNameTextField: UITextField!
@IBOutlet weak var emailTextField: UITextField!
@IBOutlet weak var passwordTextField: UITextField!
@IBOutlet weak var verifyPassTextField: UITextField!
@IBOutlet weak var firstNameTextField: UITextField!
@IBOutlet weak var lastNameTextField: UITextField!
@IBOutlet weak var stateTextField: UITextField!
@IBOutlet weak var birthdayTextField: UITextField!
let datePicker = UIDatePicker()
let id = Auth.auth().currentUser!.uid
let email = Auth.auth().currentUser!.email
let state_arr = ["", "AL", "AK", "AZ", "AR", "CA", "CO", "CT", "DE", "FL", "GA", "HI", "ID", "IL", "IN", "IA", "KS", "KY", "LA", "ME", "MD", "MA", "MI", "MN", "MS", "MO", "MT", "NE", "NV", "NH", "NJ", "NM", "NY", "NC", "ND", "OH", "OK", "OR", "PA", "RI", "SC", "SD", "TN", "TX", "UT", "VT", "VA", "WA", "WV", "WI", "WY"]
//picker view
let statePickerView = UIPickerView()
//Hold Current arr
var currentArr : [String] = []
//that hold current text field
var activeTextField : UITextField?
func createDatePicker() {
let toolbar = UIToolbar()
toolbar.sizeToFit()
let done2Button = UIBarButtonItem(title: "Select", style: .plain, target: self, action: #selector(doneTapped2))
let space2Button = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil)
toolbar.setItems([space2Button, done2Button], animated: false)
birthdayTextField.inputAccessoryView = toolbar
birthdayTextField.inputView = datePicker
datePicker.datePickerMode = .date
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
self.view.endEditing(true)
}
func textFieldShouldBeginEditing(_ textField: UITextField) -> Bool {
activeTextField = textField
switch textField {
case stateTextField:
currentArr = state_arr
default:
print("Default")
}
return true
}
func numberOfComponents(in pickerView: UIPickerView) -> Int {
return 1
}
func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
return currentArr.count
}
func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
return currentArr[row]
}
func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
print("Selected item is", currentArr[row])
activeTextField!.text = currentArr[row]
}
func createToolbar() {
let toolbar = UIToolbar()
toolbar.barStyle = .default
toolbar.sizeToFit()
let doneButton = UIBarButtonItem(title: "Select", style: .plain, target: self, action: #selector(doneTapped))
let spaceButton = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil)
let cancelButton = UIBarButtonItem(title: "Cancel", style: .plain, target: self, action: #selector(cancelTapped))
toolbar.setItems([cancelButton,spaceButton, doneButton], animated: false)
stateTextField.inputAccessoryView = toolbar
}
@objc func doneTapped() {
activeTextField?.resignFirstResponder()
}
@objc func cancelTapped() {
activeTextField?.resignFirstResponder()
}
override func viewDidLoad() {
super.viewDidLoad()
self.errorLabel.alpha = 0
self.navigationController?.setNavigationBarHidden(false, animated: false)
setUpElements()
createToolbar()
createDatePicker()
stateTextField.delegate = self
statePickerView.delegate = self
statePickerView.dataSource = self
stateTextField.inputView = statePickerView
}
func setUpElements() {
Utilities.styleFilledButton(signUpButton)
}
func validateFields() -> String? {
if firstNameTextField.text?.trimmingCharacters(in: .whitespacesAndNewlines) == "" ||
lastNameTextField.text?.trimmingCharacters(in: .whitespacesAndNewlines) == "" ||
emailTextField.text?.trimmingCharacters(in: .whitespacesAndNewlines) == "" ||
passwordTextField.text?.trimmingCharacters(in: .whitespacesAndNewlines) == "" ||
verifyPassTextField.text?.trimmingCharacters(in: .whitespacesAndNewlines) == "" ||
userNameTextField.text?.trimmingCharacters(in: .whitespacesAndNewlines) == "" ||
stateTextField.text?.trimmingCharacters(in: .whitespacesAndNewlines) == "" ||
birthdayTextField.text?.trimmingCharacters(in: .whitespacesAndNewlines) == ""
{
return "Please fill in all fields."
}
let cleanedPassword = passwordTextField.text!.trimmingCharacters(in: .whitespacesAndNewlines)
if Utilities.isPasswordValid(cleanedPassword) == false {
// Password isn't secure enough
return "Please make sure your password is at least 8 characters, contains a special character and a number."
}
if passwordTextField.text != verifyPassTextField.text {
// Password isn't secure enough
return "Passwords do not match."
}
return nil
}
@IBAction func termsButtonTapped(_ sender: Any) {
self.view.endEditing(true)
}
@IBAction func policyButtonTapped(_ sender: Any) {
self.view.endEditing(true)
}
@IBAction func signUpTapped(_ sender: Any) {
self.view.endEditing(true)
verifyState()
self.errorLabel.alpha = 0
let error = validateFields()
if error != nil {
showError(error!)
}
else {
// Create cleaned versions of the data
let firstName = firstNameTextField.text!.trimmingCharacters(in: .whitespacesAndNewlines)
let lastName = lastNameTextField.text!.trimmingCharacters(in: .whitespacesAndNewlines)
let email = emailTextField.text!.trimmingCharacters(in: .whitespacesAndNewlines)
let password = passwordTextField.text!.trimmingCharacters(in: .whitespacesAndNewlines)
let username = userNameTextField.text!.trimmingCharacters(in: .whitespacesAndNewlines)
let state = stateTextField.text!.trimmingCharacters(in: .whitespacesAndNewlines)
let birthday = birthdayTextField.text!.trimmingCharacters(in: .whitespacesAndNewlines)
// Create the user
Auth.auth().createUser(withEmail: email, password: password) { (result, err) in
// Check for errors
if err != nil {
// There was an error creating the user
self.showError("Error creating user")
}
else {
// User was created successfully, now store the first name and last name
let db = Firestore.firestore()
db.collection("users").document(Auth.auth().currentUser!.uid).setData( ["firstname":firstName, "lastname":lastName, "username":username, "email": email, "birthday":birthday, "state":state, "uid": Auth.auth().currentUser!.uid]) { (error) in
if error != nil {
// Show error message
self.showError("Error saving user data")
}
}
db.collection("usernames").document(username).setData( ["username":username, "email": email, "uid"
: Auth.auth().currentUser!.uid]) { (error) in
if error != nil {
// Show error message
self.showError("Error saving user data")
}
}
// Transition to the home screen
self.transitionToHome()
}
}
self.view.endEditing(true)
}
}
@IBAction func termsSwitch(_ sender: UISwitch) {
self.view.endEditing(true)
if (sender.isOn == true)
{signUpButton.isEnabled = true}
else if (sender.isOn == false)
{signUpButton.isEnabled = false}
}
func showError(_ message:String) {
errorLabel.text = message
errorLabel.alpha = 1
}
func transitionToHome() {
let HomeTabViewController = storyboard?.instantiateViewController(identifier: Constants.Storyboard.homeTabViewController) as? HomeTabViewController
view.window?.rootViewController = HomeTabViewController
view.window?.makeKeyAndVisible()
}
func verifyState() {
if stateTextField.text == "" {
self.showError("Select a State.")
return
}
}
func verifyPassword() {
if passwordTextField.text == verifyPassTextField.text {
}
else {
self.showError("Passwords do not match.")
return
}
}
@objc func doneTapped2() {
let formatter = DateFormatter()
formatter.dateStyle = .medium
formatter.timeStyle = .none
birthdayTextField.text = formatter.string(from: datePicker.date)
self.view.endEditing(true)
}
}