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

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

Related

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

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"].

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.

2 updates within one Firestore transaction

I am new to Firestore transaction, and would like to update a document field based the current data of the document.
My planned transaction is given below:
const cityRef = db.collection('cities').doc('SF');
try {
await db.runTransaction(async (t) => {
const doc = await t.get(cityRef);
let status = doc.data().myStatus;
if (status == "one") {
throw "err";
} else {
// run some function - next status is based on the return
let output = await someFunction();
if (output) {
await t.update(cityRef, { myStatus: "two" });
return output;
} else {
await t.update(cityRef, { myStatus: "three" });
return output;
}
}
});
console.log("transaction successful");
} catch (err) {
console.log("Alreadu updated");
output = "one";
return output;
}
My queries are given below:
As per the documentation I have returned the data after update, however it does not seem to be working as expected.
Can we have 2 updates within one single transaction (both are updating the same field in the firestore)?
Thank you
You make the following clarification in the comments above:
someFunction() does some processing on other firestore
collection/documents (not the one I am updating) and returns either
true or false.
As you read in the doc on Transactions, "Read operations must come before write operations". If you want to read some docs in the transaction, you need to use the get() method of the Transaction, like you did with the first document. You cannot call a function that is using other Firestore methods like the get() method of a DocumentReference.

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

Swift code to Add item with quantity in Firebase Database

Using Swift code 5.1 I have managed to update Firestore Database with items in current users basket but not able to add/update quantity. Currently if I wanted to add an item that already exist in the basket it simply adds another line but I wanted to just update quantity.
Can you advise me on how to create a function that adds quantity?
Here are the codes I have so far. Only relevant sections of code pasted.
Firestore DB function in my Helper file:
enum FCollectionReference: String {
case User
case Category
case Items
case Basket
case Orders
}
func FirebaseReference(_ collectionReference: FCollectionReference) -> CollectionReference {
return Firestore.firestore().collection(collectionReference.rawValue)
}
Here's the code in in my Basket Model file using
class Basket {
var id: String!
var ownerId: String!
var itemIds: [String]!
var delivery: Float!
var admin: Float!
var quantity: Int!
init() {
}
init(_dictionary: NSDictionary) {
id = _dictionary[kOBJECTID] as? String
ownerId = _dictionary[kOWNERID] as? String
itemIds = _dictionary[kITEMIDS] as? [String]
delivery = _dictionary[kDELIVERY] as? Float
admin = _dictionary[kADMIN] as? Float
quantity = _dictionary[kQUANTITY] as? Int
}
}
//MARK: Helper functions
func basketDictionaryFrom(_ basket: Basket) -> NSDictionary {
return NSDictionary(objects: [basket.id, basket.ownerId, basket.itemIds, basket.quantity], forKeys: [kOBJECTID as NSCopying, kOWNERID as NSCopying, kITEMIDS as NSCopying, kQUANTITY as NSCopying,kDELIVERY as NSCopying, kADMIN as NSCopying])
}
//MARK: - Update basket
func updateBasketInFirestore(_ basket: Basket, withValues: [String : Any], completion: #escaping (_ error: Error?) -> Void) {
FirebaseReference(.Basket).document(basket.id).updateData(withValues) { (error) in
completion(error)
Codes in Item View Control to add items to basket:
#objc func addToBasketButtonPressed() {
//check if user is logged in or show login view
if MUser.currentUser() != nil {
downloadBasketFromFirestore(MUser.currentId()) { (basket) in
if basket == nil {
self.createNewBasket()
}else {
basket?.itemIds.append(self.item.id)
self.updateBasket(basket: basket!, withValues: [kITEMIDS: basket!.itemIds])
}
}
} else {
showLoginView()
}
}
private func updateBasket(basket: Basket, withValues: [String : Any]) {
updateBasketInFirestore(basket, withValues: withValues) { (error) in
if error != nil {
self.hud.textLabel.text = "Error: \(error!.localizedDescription)"
self.hud.indicatorView = JGProgressHUDErrorIndicatorView()
self.hud.show(in: self.view)
self.hud.dismiss(afterDelay: 2.0)
print("error updating basket", error!.localizedDescription)
}else {
self.hud.textLabel.text = "Added to Basket"
self.hud.indicatorView = JGProgressHUDSuccessIndicatorView()
self.hud.show(in: self.view)
self.hud.dismiss(afterDelay: 2.0)
}
}
}
To clarify my request, what do I need to change/re-arrange in my coding so the Database Cloud Firestore is arranged in order shown in my attached screen shot. First screen shot showing current layout in the last column and I'm trying to change this to layout demonstrated in the second screen shot?
I think you are asking how to update the value in a field within a Firestore document. If not, let me know and I will update the answer.
Here's some code that updates the qty of an item in inventory. Pass in the qty to add as a + Int and then to subtract as a - Int. The structure looks like this
root
inventory
item_0
qty: 0
and the code to update the qty node is:
func incrementQty(deltaQty: Int) {
let docToUpdate = self.db.collection("inventory").document("item_0")
docToUpdate.updateData( [
"qty": FieldValue.increment( Int64(deltaQty) )
])
}
call it like this
self.incrementQty(deltaQty: 4) //adds 4 to the existing qty
previously, incrementing values had to be wrapped into a transaction to make it safe but the FieldValue makes it much easier.
I am adding another answer based on comments and question clarification. My other answer still stands as an answer but it's a different approach.
Arrays are inherently hard to work with in NoSQL databases as they are often treated as a single object. They have limited functionality opposed to collections, documents and fields, and can't directly be sorted or have items inserted. And querying is well, challenging. Firestore does a great job at providing better interoperability with arrays but there are still usually better options.
Instead of an array, I would change the structure to this:
Baskets (collection)
basket_number (document in the Baskets collection, like you have now)
items //a collection of items in the basket
item_0 //a document with the docID being the the item number
item_qty: //qty of the item
item_1
item_qty:
item_2
item_qty:
So the downside of .updateData is that if the field being updated doesn't exist, it doesn't create the field, it simply throws an error. So we need to test to see if the document exists first, if so, update with updateData, if not create the item with an initial quantity.
Here's the code that does it - note for simplicity I am ignoring the top level Basket and basket_number since you already know how to do that part and focused on the items collection and down.
func incrementQty(itemNumberToUpdate: String, deltaQty: Int) {
let docToUpdate = self.db.collection("items").document(itemNumberToUpdate)
docToUpdate.getDocument(completion: { documentSnapshot, error in
if let err = error {
print(err.localizedDescription)
return
}
if let _ = documentSnapshot?.data() {
print("item exists, update qty")
docToUpdate.updateData([
"item_qty": FieldValue.increment( Int64(deltaQty) )
], completion: { err in
if let err = err {
print("Error updating document: \(err.localizedDescription)")
} else {
print("Item qty successfully updated")
}
})
} else {
print("no item exists, need to create")
docToUpdate.setData([
"item_qty": FieldValue.increment( Int64(deltaQty) )
], completion: { err in
if let err = err {
print("Error updating document: \(err.localizedDescription)")
} else {
print("Item successfully created with initial quantity")
}
})
}
})
}
Pass in an item number and the quantity to either modify the existing qty by, or will be the initial quantity.
self.incrementQty(itemNumberToUpdate: "item_0", deltaQty: 5)

Resources