Organizing groups in Firebase Authentication on Swift - ios

I am using Firebase authentication on Swift in Xcode. I want to create "groups" for the user login so that certain users will have access to certain data. For example, in my app, I want basketball players on the basketball team to only have access to the basketball stats. Does anyone know what this is called in Firebase and how to do it?

As mentioned, the user for the authentication (Auth user) is just the user for the authentication, it does not contain much more information. Please see the attached screenshot from Firebase (Authentication):
That is the reason why we have to add a new User struct (in the users collection) which provides all these kind of information (could be name, age, groups of something.... whatever). The user document needs a reference to the Auth user. In the example I am using the user uid (#frank-van-puffelen is that a common way to use the uid or does it cause safety relevant issues?)
One side note, since we only get the entire documents and sometimes a user could have some private data that must not be available for others, it may make sense to split the struct into PublicUser and PrivateUser.
Anyway, for this example, let's create a User struct in swift
User
//
// User.swift
// Firebase User
//
// Created by Sebastian Fox on 18.08.22.
//
import Foundation
import SwiftUI
import Firebase
struct User: Codable, Identifiable, Hashable {
var id: String?
var name: String
var group: SportType
init(name: String, group: SportType, id: String?) {
self.id = id
self.name = name
self.group = group
}
init?(document: QueryDocumentSnapshot) {
let data = document.data()
guard let name = data["name"] as? String else {
return nil
}
guard let group = data["group"] as? SportType else {
return nil
}
id = document.documentID
self.name = name
self.group = group
}
enum CodingKeys: String, CodingKey {
case id
case name
case group
}
}
extension User: Comparable {
static func == (lhs: User, rhs: User) -> Bool {
return lhs.id == rhs.id
}
static func < (lhs: User, rhs: User) -> Bool {
return lhs.name < rhs.name
}
}
// I also create an enum with sort types, this is not necessarily part of the User struct. To load it to the Firestone database it must be codable.
enum SportType: String, Codable, CaseIterable {
case basektball = "Basketball"
case baseball = "Baseball"
case soccer = "Soccer"
case chess = "Chess"
case noSport = "No Sport"
}
Now, let's do the magic with the UserViewModel which contains the functions we call to work with Firebase (Firestore), e.g. signUp (here we are talking about the Auth user), signIn (Auth user again) or createNewUser (here it is our new user struct):
UserViewModel.swift
//
// UserViewModel.swift
// Firebase User
//
// Created by Sebastian Fox on 18.08.22.
//
import Foundation
import FirebaseFirestore
import Firebase
import FirebaseFirestoreSwift
class UsersViewModel: ObservableObject {
let db = Firestore.firestore()
// Published and saved to local device
#Published var users = [User]()
// Sign Up
func signUp(email: String, password: String, completion: #escaping (Bool, String)->Void) {
Auth.auth().createUser(withEmail: email, password: password) { authResult, error in
// ERROR AND SUCCESS HANDLING
if error != nil {
// ERROR HANDLING
print(error?.localizedDescription as Any)
completion(false, "ERROR")
}
// SUCCESS HANDLING
completion(true, authResult?.user.uid ?? "")
}
}
// Sign In
func signIn(email: String, password: String, completion: #escaping (Bool)->Void) {
Auth.auth().signIn(withEmail: email, password: password) { (authResult, error) in
// ERROR AND SUCCESS HANDLING
if error != nil {
// ERROR HANDLING
print(error?.localizedDescription as Any)
completion(true)
}
// SUCCESS HANDLING
completion(true)
}
}
// Sign Out
func signOut() {
try! Auth.auth().signOut()
}
// Create new user
func createNewUser(name: String, group: SportType, id: String) {
do {
let newUser = User(name: name, group: group, id: id)
try db.collection("users").document(newUser.id!).setData(from: newUser) { _ in
print("User \(name) created")
}
} catch let error {
print("Error writing user to Firestore: \(error)")
}
}
// FOR TESTING: Get a list of all users
func fetchAllUsers(_ completion: #escaping (Bool) ->Void) {
self.users = []
db.collection("users").addSnapshotListener { (querySnapshot, error) in
guard let documents = querySnapshot?.documents else {
print("No documents")
return
}
self.users = documents.map { queryDocumentSnapshot -> User in
let data = queryDocumentSnapshot.data()
let id = data["id"] as? String ?? ""
let name = data["name"] as? String ?? ""
let group = data["group"] as? String ?? ""
return User(name: name, group: SportType(rawValue: group) ?? .noSport, id: id)
}
completion(true)
}
}
}
Now you have 2 options to signUp (the Auth user) AND create a new user (based on the new user struct):
You have 2 separate views, on the first view, the user signs up, that creates the Auth user. On the second view, which is only available after signing up, the user can add data like name, group or whatever you want. (I'd prefer this option)
You handle everything in one view. You are holding all necessary data, call the signUp function and when you get the completion response, you call the function to create the user.
One last thing, since you don't get that information from Auth.auth(), if you want to be able to change these data, you'll have to fetch the user data for that specific user from the Firestore database. Of course you can save these information as values to UserDefaults (storage) while you create a new user, later you can save that information when the user logs in.
Best, Sebastian

Related

How to Implement User-Specific Favourites using Firestore?

My project contains an array of MealPlans. Right now, a user can star a MealPlan and that will update the isStarred Bool value in the MealPlan document. However, this just updates the database that every user currently accesses.
How can I code it so that a user has their own personal set of isStarred MealPlans?
I'm currently using Firebase Authentication and Firestore. This is my MealPlan struct:
struct MealPlan {
var docID:String?
var title:String?
var recipeSource:String?
var estimatedTime:String?
var coverImageView:String?
var equipment:[String]?
var instructions:[String]?
var isStarred:Bool?
}
My User struct:
struct User {
var userId:String?
var firstName:String?
var lastName:String?
var email:String?
}
My data model:
class MealPlanModel {
var delegate:MealPlanProtocol?
var listener:ListenerRegistration?
func getMealPlans(_ starredOnly:Bool = false) {
// Detach any listener
listener?.remove()
// Get a reference to the database
let db = Firestore.firestore()
var query:Query = db.collection("mealPlans")
// If filtering for starred Meal Plans, update the query
if starredOnly {
query = query.whereField("isStarred", isEqualTo: true)
}
self.listener = query.addSnapshotListener({ (snapshot, error) in
// Check for errors
if error == nil && snapshot != nil {
var mealPlans = [MealPlan]()
// Parse documents into mealPlans
for doc in snapshot!.documents {
let m = MealPlan(
docID: doc["docID"] as? String,
title: doc["title"] as! String,
recipeSource: doc["recipeSource"] as? String,
estimatedTime: doc["estimatedTime"] as? String,
coverImageView: doc["coverImageView"] as? String,
ingredientsProduce: doc["ingredientsProduce"] as? [String],
ingredientsProtein: doc["ingredientsProtein"] as? [String],
ingredientsSpices: doc["ingredientsSpices"] as? [String],
ingredientsOther: doc["ingredientsOther"] as? [String],
equipment: doc["equipment"] as? [String], instructions: doc["instructions"] as? [String],
isStarred: doc["isStarred"] as? Bool)
mealPlans.append(m)
}
// Call the delegate and pass back the notes in the main thread
DispatchQueue.main.async {
self.delegate?.mealPlansRetrieved(mealPlans: mealPlans)
}
}
})
}
func updateStarredStatus(_ docId:String, _ isStarred:Bool) {
let db = Firestore.firestore()
db.collection("mealPlans").document(docId).updateData(["isStarred":isStarred])
}
}
And the method for starring in my View Controller:
#IBAction func starButtonTapped(_ sender: Any) {
// Toggle the star filter status
isStarFiltered.toggle()
// Run the query
if isStarFiltered {
model.getMealPlans(true)
}
else {
model.getMealPlans()
}
// Update the starButton
setStarFilterButton()
}
Would it involve copying the docID of a starred MealPlan into a key in the Users struct? And then displaying those MealPlans when filtering for starred MealPlans?
Any help/guidance is much appreciated!
A solution is for each user to track their own meal plans. You could create a collection that stores the meal plan id's within the users document so it would look like this
meals
meal_0 //auto generated documentId
//meal ingredients
meal_1
//meal ingredients
meal_2
//meal ingredients
and then
users
user_0 //the documentId is the users uid
//user info
my_meals (collection)
meal_0: true
mean_2: true
That being said, it's often a good idea to keep data at a high level to reduce the amount of reads and simplify queries so this structure may be better instead of storing the users meals within the users document
starred_meals
user_0 //the documentId is the users uid
meal_0: true
mean_2: true
When a use logs in, read their document from the starred_meals collection.
To expand on that a little bit, sometimes a user will want to store other information with their meal so instead the above, how about this
starred_meals
user_0
starred_meal_0 (auto generated documentId)
meal_id: meal_0
wine_pairing: "Cabernet Sauvignon"
starred_meal_1
meal_id: meal_2
wine_pairing: "Barolo"
with that you could store all kinds of other information associated with each meal, per user

Swift UITableViewController `await` until all data is loaded before rendering, or re-render after data has been loaded

I am on Swift 4. The goal is to load all the data in an address book, before render the address book in view. In a different language such as js, I may use await in each item in the loop, before telling the view to render the rows. I am looking for the canonical way to solve this issue in Swift 4 with UITableViewController.
Right now the address book is stored in backend with Amplify and GraphQL. I have a User model of form
type User #Model {
id: ID!
name: String!
bio : String!
}
and Contact of form
type Contact #model {
ownerId: ID!
userId: ID!
lastOpened: String
}
In ContactController: UITableViewController.viewDidLoad I fetch all Contact in database where the ownerId is my user's id-token, I then create an object using this contact information. And then for each Contact object instance, I get its corresponding User in database when the object is initialized. Per this post: Wait until swift for loop with asynchronous network requests finishes executing, I am using Dispatch group, and then reload the UITableView after the loop completes and the Dispatch group has ended. But when I print to console, I see that the loop completes before the Contact object has loaded its User information.
Code snippets:
class ContactsController: UITableViewController, UISearchResultsUpdating {
var dataSource : [Contact] = []
override func viewDidLoad() {
super.viewDidLoad()
let fetchContactGrp = DispatchGroup()
fetchContactGrp.enter()
self.getMyContacts(){ contacts in
for contact in contacts {
let _contactData = Contact(
userId : contact.userId
, contactId : contact.id
, timeStamp : contact.timeStamp
, lastOpened : contact.lastOpened
, haveAccount: true
)
_contactData.loadData()
self.dataSource.append(_contactData)
}
}
fetchContactGrp.leave()
DispatchQueue.main.async{
self.tableView.reloadData()
}
}
}
The function self.getMyContacts is just a standard GraphQL query:
func getMyContacts( callBack: #escaping ([Contact]) -> Void ){
let my_token = AWSMobileClient.default().username
let contact = Contact.keys
let predicate = contact.ownerId == my_token!
_ = Amplify.API.query(from: Contact.self, where: predicate) { (event) in
switch event {
case .completed(let result):
switch result {
case .success(let cts):
/// #On success, output a user list
callBack(cts)
case .failure(let error):
break
}
case .failed(let error):
break
default:
break
}
}
}
And the Contact object loads the User data from database:
class Contact {
let userId: String!
let contactId: String!
var name : String
var bio : String
var website: String
let timeStamp: String
let lastOpened: String
init( userId: String, contactId: String, timeStamp: String, lastOpened: String, haveAccount: Bool){
self.userId = userId
self.contactId = contactId
self.timeStamp = timeStamp
self.lastOpened = lastOpened
self.haveAccount = haveAccount
self.name = ""
self.bio = ""
self.website = ""
}
func loadData(){
/// #use: fetch user data from db and populate field on initation
let _ = Amplify.API.query(from: User.self, byId: self.userId) { (event) in
switch event {
case .completed(let res):
switch res{
case .success (let musr):
if (musr != nil){
let userData = musr!
let em = genEmptyString()
self.name = (userData.name == em) ? "" : userData.name
self.bio = (userData.bio == em) ? "" : userData.bio
self.website = (userData.website == em) ? "" : userData.website
print(">> amplify.query: \(self.name)")
} else {
break
}
default:
break
}
default:
print("failed")
}
}
}
}
It's because the function getMyContacts() is performing an Async task and the control goes over that and execute the leave statement. You need to call the leave statement inside the getMyContacts() function outside the for loop.
Try the following code:
override func viewDidLoad() {
super.viewDidLoad()
let fetchContactGrp = DispatchGroup()
fetchContactGrp.enter()
self.getMyContacts(){ contacts in
for contact in contacts {
let _contactData = Contact(
userId : contact.userId
, contactId : contact.id
, timeStamp : contact.timeStamp
, lastOpened : contact.lastOpened
, haveAccount: true
)
_contactData.loadData()
self.dataSource.append(_contactData)
}
fetchContactGrp.leave()
}
fetchContactGrp.wait()
DispatchQueue.main.async{
self.tableView.reloadData()
}
}
I posted a more general version of this question here: Using `DispatchGroup` or some concurency construct to load data and populate cells in `UITableViewController` sequentially
And it has been resolved.

Add a Document's Document ID to Its Own Firestore Document - Swift 4

How do I go about adding the document ID of a document I just added to my firestore database, to said document?
I want to do this so that when a user retrieves a "ride" object and chooses to book it, I can know which specific ride they've booked.
The problem that i'm facing is that you can't get the document ID until after it's created, so the only way to add it to said document would be to create a document, read its ID, then edit the document to add in the ID. At scale this would create twice as many server calls as desired.
Is there a standard way to do this? Or a simple solution to know which "ride" the user booked and edit it accordingly in the database?
struct Ride {
var availableSeats: Int
var carType: String
var dateCreated: Timestamp
var ID: String // How do I implement this?
}
func createRide(ride: Ride, completion: #escaping(_ rideID: String?, _ error: Error?) -> Void) {
// Firebase setup
settings.areTimestampsInSnapshotsEnabled = true
db.settings = settings
// Add a new document with a generated ID
var ref: DocumentReference? = nil
ref = db.collection("rides").addDocument(data: [
"availableSeats": ride.availableSeats,
"carType": ride.carType,
"dateCreated": ride.dateCreated,
"ID": ride.ID,
]) { err in
if let err = err {
print("Error adding ride: \(err)")
completion(nil, err)
} else {
print("Ride added with ID: \(ref!.documentID)")
completion(ref?.documentID, nil)
// I'd currently have to use this `ref?.documentID` and edit this document immediately after creating. 2 calls to the database.
}
}
}
While there is a perfectly fine answer, FireStore has the functionality you need built in, and it doesn't require two calls to the database. In fact, it doesn't require any calls to the database.
Here's an example
let testRef = self.db.collection("test_node")
let someData = [
"child_key": "child_value"
]
let aDoc = testRef.document() //this creates a document with a documentID
print(aDoc.documentID) //prints the documentID, no database interaction
//you could add the documentID to an object etc at this point
aDoc.setData(someData) //stores the data at that documentID
See the documentation Add a Document for more info.
In some cases, it can be useful to create a document reference with an
auto-generated ID, then use the reference later. For this use case,
you can call doc():
You may want to consider a slightly different approach. You can obtain the document ID in the closure following the write as well. So let's give you a cool Ride (class)
class RideClass {
var availableSeats: Int
var carType: String
var dateCreated: String
var ID: String
init(seats: Int, car: String, createdDate: String) {
self.availableSeats = seats
self.carType = car
self.dateCreated = createdDate
self.ID = ""
}
func getRideDict() -> [String: Any] {
let dict:[String: Any] = [
"availableSeats": self.availableSeats,
"carType": self.carType,
"dateCreated": self.dateCreated
]
return dict
}
}
and then some code to create a ride, write it out and leverage it's auto-created documentID
var aRide = RideClass(seats: 3, car: "Lincoln", createdDate: "20190122")
var ref: DocumentReference? = nil
ref = db.collection("rides").addDocument(data: aRide.getRideDict() ) { err in
if let err = err {
print("Error adding document: \(err)")
} else {
aRide.ID = ref!.documentID
print(aRide.ID) //now you can work with the ride and know it's ID
}
}
I believe that if you use Swift's inbuilt ID generator, called UUID, provided by the Foundation Framework, this will let you do what you want to do. Please see the below code for my recommended changes. Also by doing it this way, when you first initialise your "Ride" struct, you can generate its ID variable then, instead of doing it inside the function. This is the way I generate unique ID's throughout my applications and it works perfectly! Hope this helps!
struct Ride {
var availableSeats: Int
var carType: String
var dateCreated: Timestamp
var ID: String
}
func createRide(ride: Ride, completion: #escaping(_ rideID: String, _ error: Error?) -> Void) {
// Firebase setup
settings.areTimestampsInSnapshotsEnabled = true
db.settings = settings
// Add a new document with a generated ID
var ref: DocumentReference? = nil
let newDocumentID = UUID().uuidString
ref = db.collection("rides").document(newDocumentID).setData([
"availableSeats": ride.availableSeats,
"carType": ride.carType,
"dateCreated": ride.dateCreated,
"ID": newDocumentID,
], merge: true) { err in
if let err = err {
print("Error adding ride: \(err)")
completion(nil, err)
} else {
print("Ride added with ID: \(newDocumentID)")
completion(newDocumentID, nil)
}
}
}
This is my solution which works like a charm
let opportunityCollection = db.collection("opportunities")
let opportunityDocument = opportunityCollection.document()
let id = opportunityDocument.documentID
let data: [String: Any] = ["id": id,
"name": "Kelvin"]
opportunityDocument.setData(data) { (error) in
if let error = error {
completion(.failure(error))
} else {
completion(.success(()))
}
}

Initialize object with Firestore data within a model

I'm am looking for a nice an clean way to assign and get the user/author from my post object.
I could possible loop throw each Post in the posts array and get the user data from Firestore and assign it to the post property from within the viewcontroller, but what I'm looking for is like a computed property or lazy property that gets the data from Firestore and adds it to the object automatically when Post is initialized.
I don't no if it is possible or if it is the right way to do it, been struggling trying different methods with out any success.
This is my current post model - Post.swift
struct Post {
var author: User?
let authorUID: String
let content: String
init(authorUID: String, content: String) {
self.authorUID = authorUID
self.content = content
}
var dictionary: [String:Any] {
return [
"authorUID": authorUID,
"content": content
]
}
}
In my viewController I have a array of post witch get filled by data from the Firestore database - PostViewController.swift
func loadAllPosts() {
database.collection("posts").getDocuments { (querySnapshot, error) in
if let error = error {
print("Error when loading uhuus: \(error.localizedDescription)")
} else {
self.posts = querySnapshot!.documents.flatMap({Post(dictionary: $0.data())})
DispatchQueue.main.async {
self.tableview.reloadData()
}
}
}
}
This is what I was imagining my Post model would look like - Post.swift
(This code does not work and returns a "Unexpected non-void return value in void function" error)
struct Post {
private var database = Firestore.firestore()
var author: User?
let authorUID: String
let content: String
init(authorUID: String, content: String) {
self.authorUID = authorUID
self.content = content
}
var dictionary: [String:Any] {
return [
"authorUID": authorUID,
"content": content
]
}
private var setUser: User {
database.collection("users").document(authorUID).getDocument { (document, error) in
if let user = document.flatMap({ User(dictionary: $0.data()) }) {
return user
} else {
print("Document does not exist")
}
}
}
}
If you don't have a solutions but nows this is bad practice, Then I would like to know why and what would be the best way.
You seem to be on the right track with your second code snippet. You might consider triggering the setUser function when you init a Post as that is the point where you will have a valid authorId.
The reason you are getting that error is because you are trying to return a User in your setUser function, when what you really need to do is just update your user variable on that particular Post.
I'd suggest updating like so:
struct Post {
private var database = Firestore.firestore()
var author: User?
let authorUID: String
let content: String
init(authorUID: String, content: String) {
self.authorUID = authorUID
self.content = content
//Immediately fetch the User object here
self.setUser()
}
var dictionary: [String:Any] {
return [
"authorUID": authorUID,
"content": content
]
}
private func setUser() {
database.collection("users").document(authorUID).getDocument { (document, error) in
if let user = document.flatMap({ User(dictionary: $0.data()) }) {
//Set the user on this Post
self.author = user
} else {
print("Document does not exist")
}
}
}
}
On a somewhat unrelated note, you might want to avoid having a database reference inside each Post object. It will still work if you continue the way you have it here, but it's not the Post's job to keep track of this. It'd make more sense to have it in a separate class that is built to deal with database functions.

Check if user exist with firebase 3.0 + swift

I have a app that after the user use Firebase auth it store the data on the Firebase database. Before storing the data, I want to check if the username the user give already exist in the database. So if it not exist I could give the user this unique username(like every user have a unique username). So I have a textField where the user enter his username, and then press Next. Then the app should check if the username exist or not, and tell the user if he need to change it.
So the code I used to check if the username exist:
let databaseRef = FIRDatabase.database().reference()
databaseRef.child("Users").observeSingleEventOfType(.Value, withBlock: { (snapshot) in
if snapshot.hasChild(self.usernameTextField.text!){
print("user exist")
}else{
print("user doesn't exist")
}
})
So every time the next button is pressed, this code is called. The problem with this is that the result always remain the same as the first search (even after the textField value change).
For example, if I search Jose, and Jose exist in my database so is going to print "user exist". But when I change the textField to name that don't exist, it still show "user exist".
I figured out I need to change the .Value to FIRDataEventType.Value
if (usernameTextField.text?.isEmpty == false){
let databaseRef = FIRDatabase.database().reference()
databaseRef.child("Users").observeSingleEventOfType(FIRDataEventType.Value, withBlock: { (snapshot) in
if snapshot.hasChild(self.usernameTextField.text!){
print("true rooms exist")
}else{
print("false room doesn't exist")
}
})
struct ModelUser {
var id: String
var name: String
init(data: DataSnapshot) {
// do init stuff
}
}
func isUserRegistered(with id: String, completion: #escaping (_ exists: Bool, _ user: ModelUser?) -> ()) {
DatabaseReference.users.child(id).observeSingleEvent(of: .value) { (snapshot) in
if snapshot.exists() {
// user is already in our database
completion(true, ModelUser(data: snapshot))
} else {
// not in database
completion(false, nil)
}
}
}
This worked for me in a similar situation as yours. You can also go the Rx way like this.
enum CustomError: Error {
case userNotRegistered
var localizedDescription: String {
switch self {
case .userNotRegistered:
return "Dude is not registered..."
}
}
}
func isUserRegistered(with id: String) -> Observable<(ModelUser)> {
let reference = DatabaseReference.users.child(id)
return Observable<ModelUser>.create({ observer -> Disposable in
let listener = reference.observe(.value, with: { snapshot in
if snapshot.exists() {
observer.onNext(ModelUser(data: snapshot))
observer.onCompleted()
} else {
observer.onError(CustomError.userNotRegistered)
}
})
return Disposables.create {
reference.removeObserver(withHandle: listener)
}
})
}
The key in both cases is using the .exists() method of the snapshot.

Resources