SwiftUI #Published variable not updating - firebase

I have a class with a #Published variable named userData. When the function fbLogic.getUserData() is called in this class, it updates the userData array with the data which is populated from a call to Firestore database.
The issue is that this published variable is not updating in view which access it. Or more so, these functions don't wait until the Firestore call is finished before accessing the variable. So the BarGraphView() is called with empty reports[] array, and this array isn't updated by the convertToReports() call because convertToReports() is called with an empty userData array, because fbLogic.getUserData() hasn't finished retrieving the Firebase data yet. Hopefully the code explains it better.
This is the View which is making reference to the array.
struct StatsView: View {
let ID: String;
#EnvironmentObject var statsController: StatsDataController
#State var reports: [Report] = [Report]();
#Binding var fbLogic: FirebaseLogic
var body: some View {
BarGraphView(selection: $selection, reports: $reports, originalReports: $originalReports).environmentObject(statsController).onAppear{
fbLogic.userData = fbLogic.getUserData(uid: ID); //supposed to update the userData #Published var
//These are called before fbLogic.userData has even been updated !!!
self.reports = statsController.convertToReports(users: fbLogic.userData);
print("fblogic data ", fbLogic.userData, " ", ID);};
}
This View prints out "fb logic data []", before the call to .getUserData() has even finished, and so the call to convertToReports() happens with an empty array, instead of a populated array.
Please, how do I make this view recognise that the fbLogic.userData #Pubished array has updated and call convertToReports(), and redraw the BarGraphView with the POPULATED array.
Here is part of the fbLogic class so you can understand the call to firebase:
class FirebaseLogic: ObservableObject {
#Published var userData = [UserData]()
func getUserData(uid: String) -> [UserData]{
let db = Firestore.firestore()
userData = [UserData]()
let auth = Auth.auth();
let currentUser = (auth.currentUser?.uid)!
let data = db.collection("UserData").document(currentUser).collection("Data") .getDocuments { [self] (snapshot, error) in
guard let snapshot = snapshot, error == nil else {
return
}
print("Number of documents fb logic get user data: \(snapshot.documents.count ?? -1)")
snapshot.documents.forEach({ (documentSnapshot) in
let documentData = documentSnapshot.data()
//CONVERT TO userObject here (removed for simplicity)
userData.append(userObject)
})
}
return userData
}

Try using
#ObservedObject var fbLogic: FirebaseLogic
in StatsView. Make sure you pass an #ObservedObject type to StatsView when you pass data to this view from parent view.

The reason this isn't working is that the call to getDocuments() is asynchronous. So when you return the array of mapped documents (consider using Codable), it is still empty.
Here is an updated version of your code that assigns the mapped documents to the published property.
class FirebaseLogic: ObservableObject {
#Published var userData = [UserData]()
func getUserData(uid: String) -> [UserData] {
let db = Firestore.firestore()
let localUserData = [UserData]()
let auth = Auth.auth();
let currentUser = (auth.currentUser?.uid)! // (1)
db.collection("UserData").document(currentUser).collection("Data").getDocuments { [self] (snapshot, error) in
guard let snapshot = snapshot, error == nil else {
return
}
print("Number of documents fb logic get user data: \(snapshot.documents.count ?? -1)")
snapshot.documents.forEach { documentSnapshot in
let documentData = documentSnapshot.data()
// CONVERT TO userObject here (removed for simplicity)
localUserData.append(userObject)
}
self.userData = localUserData
}
}
}
You can further improve this code by using .map() to transform your documents. See this code snippet that is part of my article about mapping Firestore documents to/from Swift:
listenerRegistration = db.collection("programming-languages")
.addSnapshotListener { [weak self] (querySnapshot, error) in
guard let documents = querySnapshot?.documents else {
self?.errorMessage = "No documents in 'programming-languages' collection"
return
}
self?.programmingLanguages = documents.compactMap { queryDocumentSnapshot in
let result = Result { try queryDocumentSnapshot.data(as: ProgrammingLanguage.self) }
...

Related

Firebase Swiftui not saving fetched data

I am trying to fetch a userID key from firebase. The code is initiated in a .OnAppear and the data is fetched. If I try to print self.appUserId inside docRef it prints out correctly. If I print it at the end like in the code below the variable is empty. appUserID is declared as:
#State private var appUserId = ""
Can someone explain how I can fetch this firebase data and save it to a string
.onAppear{
let userName = user?.email ?? ""
let docRef = db.collection("SUBSCRIBE").document(userName)
docRef.getDocument { (document, error) in
if let document = document, document.exists {
//Setting Values
let data = document.data()
self.appUserId = data?["AppUserId"] as? String ?? ""
} else {
print("Document does not exist")
}
}
print(self.appUserId)
}

SwiftUI - Get field value from a Firebase document

I'm pretty new to SwiftUI and using Firebase and have stumbled upon retrieving a value from a document on Firebase.
I've done some Googling but have yet to find a way to do this.
My code so far looks like this:
//Test1: get user info
func readUserInfo(_ uid: String) -> String {
let db = Firestore.firestore()
let docRef = db.collection("users").document(uid)
var returnThis = ""
docRef.getDocument { (document, error) -> String in
if let document = document, document.exists {
let dataDescription = document.data().map(String.init(describing:)) ?? "nil"
print("Document data: \(dataDescription)")
returnThis = dataDescription
return(dataDescription)
} else {
print("Document does not exist")
}
}
return returnThis
}
//Test2: get user info
func readUserInfo(_ uid: String) -> String {
let db = Firestore.firestore()
let docRef = db.collection("users").document(uid)
var property = "not found"
docRef.getDocument { (document, error) in
if let document = document, document.exists {
property = document.get("phoneNumber") as! String
}
}
return property
}
Test1:
Got an error saying "Declared closure result 'String' is incompatible with contextual type 'Void'"
Test2:
The returned value is always "not found", so I guess assigning document.get("phoneNumber") as! String to the property variable didn't work.
In both cases, I was able to print the value out. However, I want the value to be passed to a Text element so I can display it on the interface.
Could someone please help? Thanks so much in advance.
The reason why the value is never found is because the network request you are making takes time to retrieve that data. By the time it reaches the return statement the data from firebase has not been been retrieved yet so it will just return the value it was initially set to which was an empty string. What you should do is add a completion handler to the function so that once the data is finally retrieved you can then return it.
struct ContentView: View {
#State private var text: String = ""
var body: some View {
Text(text)
.onAppear {
readUserInfo("sydQu5cpteuhPEgmHepn") { text in
self.text = text
}
}
}
func readUserInfo(_ uid: String, _ completion: #escaping (String) -> Void) {
let db = Firestore.firestore()
let docRef = db.collection("users").document(uid)
docRef.getDocument { document, error in
if let document = document, document.exists {
let dataDescription = document.data().map(String.init(describing:)) ?? "nil"
print("Document data: \(dataDescription)")
completion(dataDescription)
} else {
completion("Document does not exist")
}
}
}
}

I cannot read data from Firebase without a List or ForEach

I have an application that is:
The first view is a list of stores (the list has been read from Firebase successfully) because there is a (List)
The second view is the store details (here, I couldn't read the data from Firebase)
#ObservedObject var viewModel = StoreViewModel()
var body: some View {
ScrollView {
VStack {
Text(viewModel.stores.name)}}
How read this data from Firebase ?
class StoreViewModel: ObservableObject {
#Published var stores = [StoreView]()
private var db = Firestore.firestore()
func fetchData() {
db.collection("Stores").addSnapshotListener{(querySnapshot, error)in
guard let documents = querySnapshot?.documents else {
print("No documents in Firebease")
return
}
self.stores = documents.compactMap { queryDocumentSnapshot -> StoreView? in
return try? queryDocumentSnapshot.data(as: StoreView.self)
}
}
}
}
First, your view model never reads from the database because fetchData() is never called. Therefore, consider calling it automatically in the view model's initializer:
class StoreViewModel: ObservableObject {
#Published var stores = [StoreView]()
private let db = Firestore.firestore()
init() {
db.collection("Stores").addSnapshotListener{ (querySnapshot, error) in
guard let documents = querySnapshot?.documents else {
print("No documents in Firebease")
if let error = error {
print(error)
}
return
}
self.stores = documents.compactMap { queryDocumentSnapshot -> StoreView? in
return try? queryDocumentSnapshot.data(as: StoreView.self)
}
}
}
}
Second, stores is an array of StoreView objects, so you must pick one the elements of the array to display any data from it:
Text(viewModel.stores[0].name) // this will display the name of the first store added to the array

Is It Possible To Get The ID Before It Was Added?

I know that in Realtime Database I could get the push ID before it was added like this:
DatabaseReference databaseReference= FirebaseDatabase.getInstance().getReference();
String challengeId=databaseReference.push().getKey();
and then I could add it using this ID.
Can I also get it in the Cloud Firestore?
This is covered in the documentation. See the last paragraph of the add a document section.
DocumentReference ref = db.collection("my_collection").doc();
String myId = ref.id;
const db = firebase.firestore();
const ref = db.collection('your_collection_name').doc();
const id = ref.id;
You can do this in following manner (code is for AngularFire2 v5, which is similar to any other version of firebase SDK say web, node etc.)
const pushkey = this.afs.createId();
const project = {' pushKey': pushkey, ...data };
this.projectsRef.doc(pushkey).set(project);
projectsRef is firestore collection reference.
data is an object with key, value you want to upload to firestore.
afs is angularfirestore module injected in constructor.
This will generate a new document at Collection called projectsRef, with its id as pushKey and that document will have pushKey property same as id of document.
Remember, set will also delete any existing data
Actually .add() and .doc().set() are the same operations. But with .add() the id is auto generated and with .doc().set() you can provide custom id.
Firebase 9
doc(collection(this.afs, 'posts')).id;
The simplest and updated (2022) method that is the right answer to the main question:
"Is It Possible To Get The ID Before It Was Added?"
v8:
// Generate "locally" a new document in a collection
const document = yourFirestoreDb.collection('collectionName').doc();
// Get the new document Id
const documentUuid = document.id;
// Sets the new document with its uuid as property
const response = await document.set({
uuid: documentUuid,
...
});
v9:
// Get the collection reference
const collectionRef = collection(yourFirestoreDb,'collectionName');
// Generate "locally" a new document for the given collection reference
const docRef = doc(collectionRef);
// Get the new document Id
const documentUuid = docRef.id;
// Sets the new document with its uuid as property
await setDoc(docRef, { uuid: documentUuid, ... })
IDK if this helps, but I was wanting to get the id for the document from the Firestore database - that is, data that had already been entered into the console.
I wanted an easy way to access that ID on the fly, so I simply added it to the document object like so:
const querySnapshot = await db.collection("catalog").get();
querySnapshot.forEach(category => {
const categoryData = category.data();
categoryData.id = category.id;
Now, I can access that id just like I would any other property.
IDK why the id isn't just part of .data() in the first place!
For node.js runtime
const documentRef = admin.firestore()
.collection("pets")
.doc()
await admin.firestore()
.collection("pets")
.doc(documentRef.id)
.set({ id: documentRef.id })
This will create a new document with random ID, then set the document content to
{ id: new_document_id }
Hope that explains well how this works
Unfortunately, this will not work:
let db = Firestore.firestore()
let documentID = db.collection(“myCollection”).addDocument(data: ["field": 0]).documentID
db.collection(“myOtherCollection”).document(documentID).setData(["field": 0])
It doesn't work because the second statement executes before documentID is finished getting the document's ID. So, you have to wait for documentID to finish loading before you set the next document:
let db = Firestore.firestore()
var documentRef: DocumentReference?
documentRef = db.collection(“myCollection”).addDocument(data: ["field": 0]) { error in
guard error == nil, let documentID = documentRef?.documentID else { return }
db.collection(“myOtherCollection”).document(documentID).setData(["field": 0])
}
It's not the prettiest, but it is the only way to do what you are asking. This code is in Swift 5.
docs for generated id
We can see in the docs for doc() method. They will generate new ID and just create new id base on it. Then set new data using set() method.
try
{
var generatedID = currentRef.doc();
var map = {'id': generatedID.id, 'name': 'New Data'};
currentRef.doc(generatedID.id).set(map);
}
catch(e)
{
print(e);
}
For the new Firebase 9 (January 2022). In my case I am developing a comments section:
const commentsReference = await collection(database, 'yourCollection');
await addDoc(commentsReference, {
...comment,
id: doc(commentsReference).id,
date: firebase.firestore.Timestamp.fromDate(new Date())
});
Wrapping the collection reference (commentsReference) with the doc() provides an identifier (id)
To get ID after save on Python:
doc_ref = db.collection('promotions').add(data)
return doc_ref[1].id
In dart you can use:
`var itemRef = Firestore.instance.collection("user")
var doc = itemRef.document().documentID; // this is the id
await itemRef.document(doc).setData(data).then((val){
print("document Id ----------------------: $doc");
});`
You can use a helper method to generate a Firestore-ish ID and than call collection("name").doc(myID).set(dataObj) instead of collection("name").add(dataObj). Firebase will automatically create the document if the ID does not exist.
Helper method:
/**
* generates a string, e.g. used as document ID
* #param {number} len length of random string, default with firebase is 20
* #return {string} a strich such as tyCiv5FpxRexG9JX4wjP
*/
function getDocumentId (len = 20): string {
const list = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNPQRSTUVWXYZ123456789";
let res = "";
for (let i = 0; i < len; i++) {
const rnd = Math.floor(Math.random() * list.length);
res = res + list.charAt(rnd);
}
return res;
}
Usage: const myId = getDocumentId().
This is possible now (v9 2022 update) by generating Id locally:
import { doc, collection, getFirestore } from 'firebase/firestore'
const collectionObject = collection(getFirestore(),"collection_name")
const docRef = doc(collectionObject)
console.log(docRef.id) // here you can get the document ID
Optional: After this you can create any document like this
setDoc(docRef, { ...docData })
Hope this helps someone. Cheers!
This works for me. I update the document while in the same transaction. I create the document and immediately update the document with the document Id.
let db = Firestore.firestore().collection(“cities”)
var ref: DocumentReference? = nil
ref = db.addDocument(data: [
“Name” : “Los Angeles”,
“State: : “CA”
]) { err in
if let err = err {
print("Error adding document: \(err)")
} else {
print("Document added with ID: \(ref!.documentID)")
db.document(ref!.documentID).updateData([
“myDocumentId” : "\(ref!.documentID)"
]) { err in
if let err = err {
print("Error updating document: \(err)")
} else {
print("Document successfully updated")
}
}
}
}
Would be nice to find a cleaner way to do it but until then this works for me.
In Node
var id = db.collection("collection name").doc().id;
If you don't know what will be the collection at the time you need the id:
This is the code used by Firestore for generating ids:
const generateId = (): string => {
// Alphanumeric characters
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
let autoId = '';
for (let i = 0; i < 20; i++) {
autoId += chars.charAt(Math.floor(Math.random() * chars.length));
}
// assert(autoId.length === 20, "Invalid auto ID: " + autoId);
return autoId;
};
References:
Firestore: Are ids unique in the collection or globally?
https://github.com/firebase/firebase-js-sdk/blob/73a586c92afe3f39a844b2be86086fddb6877bb7/packages/firestore/src/util/misc.ts#L36

Casting Firebase Snapshot to Swift 3 object

I am retrieving Objects from the Firebase DB and I need to cast them to a custom struct class object
Class:
struct Request {
var Address: String!
var Position: Position!
var RequestID: String!
var Status: String!
}
The function that gets the snapshot from my Firebase DB:
self.ref.child("requests").observe(.childAdded, with: { snapshot in
//I need to cast this snapshot object to a new Request object here
let dataChange = snapshot.value as? [String:AnyObject]
print(dataChange)
})
How can I get this done?
A couple of things. Firebase doesn't have objects - it's a JSON structure. When you get the snapshot.value like so:
let dataChange = snapshot.value as? [String:AnyObject]
The [String: AnyObject] is defining the data as a Dictionary.
You can then access the key:value pairs in dataChange like this
let address = dataChange["address"]
and
let position = dataChange["position"]
From there you can either create new objects and populate them within the closure (adding them to an array for example) or put more intelligence in the object and pass the dictionary and let the object populate itself.
The following is pseudo code but it presents the process:
//create the object and populate it 'manually'
self.ref.child("requests").observe(.childAdded, with: { snapshot in
let dataChange = snapshot.value as? [String:AnyObject]
let aRequest = Request()
aRequest.address = dataChange["address"]
aRequest.position = dataChange["position"]
self.requestArray.append(aRequest)
})
or
Class Request {
var address = ""
var position = ""
func initWithDict(aDict: [String: AnyObject]) {
self.address = aDict["address"]
self.position = aDict["position"]
}
}
//let the object populate itself.
self.ref.child("requests").observe(.childAdded, with: { snapshot in
let dataChange = snapshot.value as? [String:AnyObject]
let aRequest = Request(initWithDict: dataChange)
self.requestArray.append(aRequest)
})
If your struct has a lot of fields, it's easier to do like this (Swift 4+):
struct Request: Decodable {
var Address: String
var RequestID: String
var Status: String
}
self.ref.child("requests").observe(.childAdded, with: { snapshot in
guard let data = try? JSONSerialization.data(withJSONObject: snapshot.value as Any, options: []) else { return }
let yourStructObject = try? JSONDecoder().decode(Request.self, from: data)
}

Resources