When data is called from Firebase Database, all of the data is called, not the one associated with the user - firebase

For some reason, when the data is called inside the collection of users, all of the data is being called.
Here is the database:
Here is the code :
import SwiftUI
import Firebase
struct AccountView: View {
#State var name = ""
var body: some View {
NavigationView {
ZStack {
VStack {
//Name
Text("Welcome \(name)")
.font(.title)
//Update Info
Button {
update.toggle()
} label: {
Text("Update My Info")
}
.buttonStyle(GradientButtonStyle())
.padding()
}
.navigationTitle("Account")
.onAppear(perform: {
downloadNameServerData()
})
}
}
}
private func downloadNameServerData() {
if !name.isEmpty { return }
let db = Firestore.firestore()
db.collection("users").document("names")
.addSnapshotListener { documentSnapshot, error in
guard let document = documentSnapshot else {
print("Error fetching document: \(error!)")
return
}
guard let Name = document.data() else {
print("Document data was empty.")
return
}
name = Name
print("Current data: \(name)")
}
}
Inside the console; when I do print(name) it ends up printing all of the user's names that are stored inside the database. If you look at the second image, you can see that the name is "Jeff Bezos" but in the database, the name saved to that user is "Bob the Builder"
It isn't that the code has any errors, it's just that all of the users' names that are saved in the database are being called upon when I just want the one that is currently logged in.

This code db.collection("users").addSnapshotListener is loading all the documents from the users collection.
If you only want to load a single user doc, see the first code snippet in the documentation on getting realtime updates a single document.
The Name in your screenshot is a field inside a document, it is not a document itself. You can access the Name field by document.data()["Name"].

Related

SwiftUI - How to add sub-collection + document to existing document in Firestore

I'm trying to figure out how to add a new sub-collection + document to an already existing document in Firestore. Here's a quick idea of the db:
orgs <-Collection
Acme <-Document (need to grab documentID from here and pass to function)
employees <-Sub-Collection
Marge Simpson <-Document
Homer Simpson <-Document
Acme2 <-Document
The basic idea is:
The user is presented with a list of company names.
User clicks on Acme, is presented with a list of employees that work for Acme.
There will be some way to add a new employee to Acme.
This will likely be a "Add Employee" button on the navigation bar that opens a sheet with text fields where you enter employee information.
When that data is saved, it should save to a new document under orgs > Acme > Employees
My current code, when run, will add a new doc at orgs/ blank doc /employees. I can't figure out how to grab the document ID of the org I'm currently looking at, in this case Acme, and pass that org document ID to the function so it adds the new employee to the correct org (Acme in this example).
Here's my view models:
Org View Model:
class OrgViewModel: ObservableObject {
#Published var orgs = [Org]()
#Published var newOrg: Org
init(newOrg: Org = Org(orgName: "", orgCity: "")) {
self.newOrg = newOrg
}
private var db = Firestore.firestore()
func fetchOrgData() {
db.collection("orgs").addSnapshotListener { (querySnapshot, error) in
guard let documents = querySnapshot?.documents
else {
print("No Documents")
return
}
self.orgs = documents.compactMap { (queryDocumentSnapshot) -> Org? in
return try? queryDocumentSnapshot.data(as: Org.self)
}
}
}
}
Employee View Model:
class EmployeeViewModel: ObservableObject {
#Published var employees = [Employee]()
#Published var newEmployee: Employee
init(newEmployee: Employee = Employee(firstName: "", lastName: "", orgName: "")) {
self.newEmployee = newEmployee
}
private var db = Firestore.firestore()
func addEmployeeData(newEmployee: Employee) {
do {
let orgRef = db.collection("orgs").document() // <--How do I pass org documentID here??
let _ = try orgRef.collection("employees").addDocument(from: newEmployee)
}
catch {
print(error)
}
}
func fetchEmployeeData() {
db.collectionGroup("employees").addSnapshotListener { (querySnapshot, error) in
guard let documents = querySnapshot?.documents
else {
print("No Documents")
return
}
self.employees = documents.compactMap { (queryDocumentSnapshot) -> Season? in
return try? queryDocumentSnapshot.data(as: Employee.self)
}
}
}
}
This needs to be able to read an existing documentID from an existing org and pass that documentID to addEmployeeData function. I'm new to Swift, so any help is greatly appreciated.

How to prevent Firestore write race conditions for a reservation button

Summary
I'm developing an application where users can reserve and cancel reservations for classes. In a ReservationButtonView I two buttons that add and remove a user to a workout class respectively. Currently the button I show is based off whether the user's Firebase Auth uid is listed in a Firestore document.
I was having issues when rapidly tapping on the reservation button. Specifically, the reservationCnt would become inaccurate by showing more or less than the actual users reserved for a class.
The only way I have found to resolve this is be using a Firestore transaction that checks to see if a user is in a workout class already. If they are, addReservation() now does nothing. If they aren't, removeReservation() would also do nothing.
At first I thought I could just disable the button and via the logic still in place the code below (.disabled()), but that alone didn't work as I ran into the above described race conditions. What I found out is that arrayUnion and arrayRemove still succeed even when the object I'm looking to add is there and not there respectively. Meaning it is possible for my transaction to not remove a reservedUser that isn't there and also decrease the reservationCnt which can leave me with say no reserved users and a reservationCnt of -1
The Ask
Is there a better way to handle this reservation process? Can I accomplish this without a transaction for at least the removal of users in some way. Ideally, I'd like to have a spinner replace the button as I add or remove a user's reservation to indicate to the user that the app is processing the request. Perhaps I need two variables to manage the disabled() state instead of one?
MVVM Code Snippets
NOTE: I pulled out some button styling to make the code a bit less verbose
ReservationButtonView
struct ReservationButtonView: View {
var workoutClass: WorkoutClass
#ObservedObject var viewModel: WorkoutClassViewModel
#EnvironmentObject var authViewModel: AuthViewModel
var body: some View {
if checkIsReserved(uid: authViewModel.user?.uid ?? "", reservedUsers: workoutClass.reservedUsers ?? []) {
Button(action: {
viewModel.isDisabled = true
viewModel.removeReservation(
documentId: workoutClass.id!,
reservedUserDetails: ["uid": authViewModel.user?.uid as Any, "photoURL": authViewModel.user?.photoURL?.absoluteString ?? "" as Any, "displayName": authViewModel.user?.displayName ?? "Bruin Fitness Member" as Any],
uid: authViewModel.user?.uid ?? "")
}){
Label(
title: { Text("Cancel Reservation")
.font(.title) },
icon: { Image(systemName: "person.badge.minus")
.font(.title) }
)
}.disabled(viewModel.isDisabled)
} else{
Button(action: {
viewModel.isDisabled = true
viewModel.addReservation(
documentId: workoutClass.id!,
reservedUserDetails: ["uid": authViewModel.user?.uid as Any, "photoURL": authViewModel.user?.photoURL?.absoluteString ?? "" as Any, "displayName": authViewModel.user?.displayName ?? "Bruin Fitness Member" as Any],
uid: authViewModel.user?.uid ?? "")
}){
Label(
title: { Text("Reserve")
.font(.title) },
icon: { Image(systemName: "person.badge.plus")
.font(.title) }
)
}
.disabled(viewModel.isDisabled)
}
}
}
func checkIsReserved(uid: String, reservedUsers: [reservedUser]) -> Bool {
return reservedUsers.contains { $0.uid == uid }
}
WorkoutClassModel
struct reservedUser: Codable, Identifiable {
var id: String = UUID().uuidString
var uid: String
var photoURL: URL?
var displayName: String?
enum CodingKeys: String, CodingKey {
case uid
case photoURL
case displayName
}
}
struct WorkoutClass: Codable,Identifiable {
#DocumentID var id: String?
var reservationCnt: Int
var time: String
var workoutType: String
var reservedUsers: [reservedUser]?
enum CodingKeys: String, CodingKey {
case id
case reservationCnt
case time
case workoutType
case reservedUsers
}
}
WorkoutClassViewModel
class WorkoutClassViewModel: ObservableObject {
#Published var isDisabled = false
private var db = Firestore.firestore()
func addReservation(documentId: String, reservedUserDetails: [String: Any], uid: String){
let incrementValue: Int64 = 1
let increment = FieldValue.increment(incrementValue)
let addUser = FieldValue.arrayUnion([reservedUserDetails])
let classReference = db.document("schedules/Redwood City/dates/\(self.stateDate.dbDateFormat)/classes/\(documentId)")
db.runTransaction { transaction, errorPointer in
let classDocument: DocumentSnapshot
do {
print("Getting classDocument for docId: \(documentId) in addReservedUser()")
try classDocument = transaction.getDocument(classReference)
} catch let fetchError as NSError {
errorPointer?.pointee = fetchError
return nil
}
guard let workoutClass = try? classDocument.data(as: WorkoutClass.self) else {
let error = NSError(
domain: "AppErrorDomain",
code: -3,
userInfo: [
NSLocalizedDescriptionKey: "Unable to retrieve workoutClass from snapshot \(classDocument)"
]
)
errorPointer?.pointee = error
return nil
}
let isReserved = self.checkIsReserved(uid: uid, reservedUsers: workoutClass.reservedUsers ?? [])
if isReserved {
print("user is already in class so therefore can't be added again")
return nil
} else {
transaction.updateData(["reservationCnt": increment, "reservedUsers": addUser], forDocument: classReference)
return nil
}
} completion: { object, error in
if let error = error {
print(error.localizedDescription)
self.isDisabled = false
} else {
print("Successfully ran transaction with object: \(object ?? "")")
self.isDisabled = false
}
}
}
func removeReservation(documentId: String, reservedUserDetails: [String: Any], uid: String){
let decrementValue: Int64 = -1
let decrement = FieldValue.increment(decrementValue)
let removeUser = FieldValue.arrayRemove([reservedUserDetails])
let classReference = db.document("schedules/Redwood City/dates/\(self.stateDate.dbDateFormat)/classes/\(documentId)")
db.runTransaction { transaction, errorPointer in
let classDocument: DocumentSnapshot
do {
print("Getting classDocument for docId: \(documentId) in addReservedUser()")
try classDocument = transaction.getDocument(classReference)
} catch let fetchError as NSError {
errorPointer?.pointee = fetchError
return nil
}
guard let workoutClass = try? classDocument.data(as: WorkoutClass.self) else {
let error = NSError(
domain: "AppErrorDomain",
code: -3,
userInfo: [
NSLocalizedDescriptionKey: "Unable to retrieve reservedUsers from snapshot \(classDocument)"
]
)
errorPointer?.pointee = error
return nil
}
let isReserved = self.checkIsReserved(uid: uid, reservedUsers: workoutClass.reservedUsers ?? [] )
if isReserved {
transaction.updateData(["reservationCnt": decrement, "reservedUsers": removeUser], forDocument: classReference)
return nil
} else {
print("user not in class so therefore can't be removed")
return nil
}
} completion: { object, error in
if let error = error {
print(error.localizedDescription)
self.isDisabled = false
} else {
print("Successfully ran removeReservation transaction with object: \(object ?? "")")
self.isDisabled = false
}
}
}
func checkIsReserved(uid: String, reservedUsers: [reservedUser]) -> Bool {
return reservedUsers.contains { $0.uid == uid }
}
}
App screenshot
Reservation button is the green/grey button at the bottom of the view
As this is a race condition, You have already acknowledged the use of Transactions for the update which is the most desirable as this can ensure the update is successful before allowing the App to change button status.
I.e. by using a transaction and only updating the UI Button state on success, which is explained here
The recommendation is to keep the state of the button mapped to what is in the document, therefore you are likely to exceed rate limits by updating the same field continuously based on the flipping of the button.
Another way to handle this tracking of the state of enrollment is to add a new document that indicates the state of the enrollment for the user to a collection that is the class they are enrolling in.
I.e. Rather than having the class user enrolling into being a document, make that a collection and each time the enrollment state changes, write a new document. This will allow for updates to occur without using transactions and the current state of enrollments is contained within the latest document. This latest document can be read and used as the status of the button within the App with the added benefit that the state will always update to the status contained within Firestore.
I ended up resolving this by adding a disable check conditional before the conditional that decides whether to show the "Reserve" or "Cancel" button.
This way when my Firestore transaction is running the user will see a spinner instead and can't monkey test the button. The spinner helps to show that the reservation operation is in progress. When the transaction hits its completion block I disable the isDisabled Bool and the listener is in sync (the user then sees the newly toggled button state)
if workoutClassVM.isDisabled {
ProgressView()
.progressViewStyle(CircularProgressViewStyle(tint: Color("bruinGreenColor")))
} else if checkIsReserved(uid: authVM.user?.uid ?? "", reservedUsers: workoutClass.reservedUsers ?? []) {
...

How do I map my ViewModel's ID to the Document ID in Firestore?

I have the fetch Data code here, but I don't understand how I am supposed to delete documents without setting the ID to the Document's ID. I was following this tutorial here. https://medium.com/swift-productions/swiftui-easy-to-do-list-with-firebase-2637c878cf1a I'm assuming I need to do so in the data mapping but I don't understand how with this code. I want to remove a todo from a SwiftUI list and also delete it's entire Firestore Document.
func fetchData() {
db.collection("todos").addSnapshotListener { (querySnapshot, error) in
guard let documents = querySnapshot?.documents else {
print("No documents")
return
}
self.todos = documents.map { (QueryDocumentSnapshot) -> Todo in
let data = QueryDocumentSnapshot.data()
let todoDetails = data["todo"] as? String ?? ""
return Todo(todoDetais: todoDetails)
}
}
}
View Model
struct Todo: Codable, Identifiable {
var id: String = UUID().uuidString
var todoDetais: String?
}
I recommend using Codable to map your Firestore documents to Swift structs. This will make your code easier to write, less prone to errors, and more type-safe.
Specifically, it will also enable you to use #DocumentID to map the Firestore document ID to the id attribute of your Swift struct.
Here's a quick example:
struct Book: Codable {
#DocumentID var id: String?
var title: String
var numberOfPages: Int
var author: String
}
func fetchBook(documentId: String) {
let docRef = db.collection("books").document(documentId)
docRef.getDocument { document, error in
if let error = error as NSError? {
self.errorMessage = "Error getting document: \(error.localizedDescription)"
}
else {
if let document = document {
do {
self.book = try document.data(as: Book.self)
}
catch {
print(error)
}
}
}
}
}
For more details, see this comprehensive guide I wrote about mapping Firestore documents to Swift structs (and back).
For more information about how to delete a Firestore document from a SwiftUI app, check out this article

How can I read the value of a field in Firestore (Swift)

I want to read out the Value of an Field of my document (in Firebase Firestore with SwiftUI).
What I already have I this:
let value = myDataBase
// My Database instance
//
value.collection("My Collection").whereField("Code", isEqualTo: codeTextInput)
.getDocuments() { (querySnapshot, err) in
if let err = err {
print("Error getting documents: \(err)")
} else {
for document in querySnapshot!.documents {
print("\(document.documentID) => \(document.data())")
}
}
}
(This Code works fine)
And now I want to store the Value of all Documents, which are in my collection and have for the key "Code" the value, which is typed in. But I want to store the Data for the key "Wert"
When I've saved it, I want to use it as an User-Default...
Btw. I don’t want collect more then 1 item with this code, I just want that this item which I collect is the right.
Let sum it up:
You want all documents in your collection with a certain value to be fetched
You want to save all of these values and be able to access them.
I can only recommend working with objects in this scenario. Let's make an example:
Lets import all modules
import Foundation
import Firebase
import FirebaseFirestoreSwift
import FirebaseStorage
import Combine
First we declare the structure:
https://firebase.google.com/docs/firestore/manage-data/add-data#custom_objects
public struct MyObject: Codable {
let id: String
let code: String?
// Needed to identify them in Firestore
enum CodingKeys: String, CodingKey {
case id
case code = "code"
}
}
Now we access it and generate an Object for each document we can fetch that contains your desired value:
https://firebase.google.com/docs/firestore/query-data/get-data#custom_objects
var myArray: Array<MyObject> = [] // Empty array where we will store all objects in
var codeTextInput = "Test"
// Fetch only desired documents
let db = Firestore.firestore()
let docRef = db.collection("My Collection").whereField("Code", isEqualTo: codeTextInput)
func getDocumentsAsObjects() { // docRef.getDocuments Needs to be in function or else: Expressions are not allowed at the top level
docRef.getDocuments { (querySnapshot, err) in //getDocuments (s) as in multiple
if let err = err {
print("Error getting documents: \(err)")
} else {
for document in querySnapshot!.documents { // iterate them and add them to your array
let result = Result {
try document.data(as: MyObject.self)
}
switch result {
case .success(let myObject):
if let myObject = myObject {
myObject.id = document!.documentID // Get the ID of the Document as we might need it later
myArray.append(myObject) // Save the document into your array
} else {
// A nil value was successfully initialized from the DocumentSnapshot,
// or the DocumentSnapshot was nil.
print("Document does not exist")
}
case .failure(let error):
// A `MyObject` value could not be initialized from the DocumentSnapshot.
print("Error decoding city: \(error)")
}
}
}
}
}
Now you have your Objects in your array and can access them

SwiftUI + Firebase - Listener not listening for changes?

I've set up a listener, but it doesn't seem to be changing according to changes in the data. The flow is the following:
If userCustomHabit is empty, user sees a button
When clicked, user can enter text in a TextField from a sheet to add to userCustomHabit (an array of strings)
Now that userCustomHabit is not empty, they should see something else
However, the problem I'm seeing is that userCustomHabits isn't updating in the view itself even though it is updating in the Firestore database.
Anyone know why this is? Included code below:
View
#ObservedObject var viewModel = RoutinesViewModel()
Group {
if self.viewModel.userCustomHabits.isEmpty {
Button(action: {
self.showCreateSheet.toggle()
}) {
Text("Create your own habits")
.font(Font.custom("Roboto-Regular", size: 20))
.frame(width: geometry.size.width * 88/100, height: 200)
.foregroundColor(.black)
.background(Color.init(UIColor.systemGray5))
.cornerRadius(40)
.overlay(
RoundedRectangle(cornerRadius: 40)
.stroke(style: StrokeStyle(lineWidth: 2, dash: [20]))
.foregroundColor(Color.init(UIColor.systemGray3))
)
}
}
else {
// Something else
}
}
.onAppear(perform: self.viewModel.newHabitsListener)
Sheet
VStack {
TextField("Enter text", text: $enteredText)
Button("Add Habit") {
self.viewModel.createNewHabits(newHabit: self.enteredText)
}
}
View Model
#Published var userCustomHabits = [String]()
func newHabitsListener() {
db.collection("users").document(currUser?.uid ?? "").addSnapshotListener { documentSnapshot, error in
guard let document = documentSnapshot else {
print("Error fetching document: \(error!)")
return
}
guard let data = document.data() else {
print("Document data was empty.")
return
}
DispatchQueue.main.async {
self.userCustomHabits = data["userCustomHabits"] as! [String]
}
}
}
func createNewHabits(newHabit: String) {
db.collection("users").document(currUser?.uid ?? "").updateData(["userCustomHabits": FieldValue.arrayUnion([newHabit])])
}
So I played around with your code a bit (since the code sample is incomplete, I had to make a few assumptions), and it seems like you might never have created the document you're writing to in the first place.
updateData only updates existing documents (see the documentation). To create a new document, use setData (see the documentation)
When changing your code form updateData to setData, the listener kicked in as expected.
However, it might be better to add a sub-collection customHabits to each user document. This way, adding new habits is as simple as adding a new document, which also makes querying a lot easier.

Resources