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

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.

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

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

MVVM with Repository/Firestore - Where is the best place to store different queried arrays from a single collection?

I am building a ToDo List App using Firestore based on this google tutorial, with a SwiftUI app using an MVVM/repository pattern, that uses one load query to find all tasks ("actions"), and I'm trying to set it up so I can have multiple date-based queries (e.g. display dates for today, for next week, possibly on the same screen)
The current version I'm working on has one single "loadData" function in the repository that is saved to a single published "actions" variable, and called when this is initialized.
class ActionRepository: ObservableObject, ActionStoreType {
let db = Firestore.firestore()
#Published var actions = [Action]()
init() {
loadData()
}
func loadData() {
let userId = Auth.auth().currentUser?.uid
db.collection("action")
.order(by: "createdTime")
.whereField("userId", isEqualTo: userId!)
.addSnapshotListener { (querySnapshot, error) in
if let querySnapshot = querySnapshot {
self.actions = querySnapshot.documents.compactMap { document in
do {
let x = try document.data(as: Action.self)
return x
}
catch {
print(error)
}
return nil
}
}
}
}
My view model just calls the repository with no parameters.
class ActionListViewModel: ObservableObject {
#Published var actionRepository = ActionRepository()
#Published var actionCellViewModels = [ActionCellViewModel]()
private var cancellables = Set<AnyCancellable>()
init() {
actionRepository.$actions.map { actions in
actions.map { action in
ActionCellViewModel(action: action)
}
}
.assign(to: \.actionCellViewModels, on: self)
.store(in: &cancellables)
}
I want to add a function to load data by date that I can call as many times as I want:
func loadMyDataByDate2(from startDate: Date, to endDate: Date? = nil) {
let userId = Auth.auth().currentUser?.uid
let initialDate = startDate
var finalDate: Date
if endDate == nil {
finalDate = Calendar.current.date(byAdding: .day, value: 1, to: initialDate)!
} else {
finalDate = endDate!
}
db.collection("action")
.order(by: "createdTime")
.whereField("userId", isEqualTo: userId!)
.whereField("startDate", isGreaterThanOrEqualTo: initialDate)
.whereField("startDate", isLessThanOrEqualTo: finalDate)
.addSnapshotListener { (querySnapshot, error) in
if let querySnapshot = querySnapshot {
self.actions = querySnapshot.documents.compactMap { document in
do {
let x = try document.data(as: Action.self)
return x
}
catch {
print(error)
}
return nil
}
}
}
But I don't know the best way to do this. If I want my view model to have three lists of tasks: one for today, one for the rest of the week, and one for next week, what would be the best way to do this?
Should I create separate variables in the repository or the View Model to store these different lists of actions?
Or add date variables for the repository so I call multiple instances of it within the View Model?
I just want to make sure I'm not going down an unwise path with how I start building this out.
I ended up doing something based on Peter's suggestion. I am getting all these filtered lists in my ViewModel. Instead of taking from the repository and storing them all in one ActionCellViewModel property, I created four different ActionCellViewModel properties.
I have four different functions in my initializer code now, and each one takes the list of actions, filters it based on date and completion status, and assigns it to the appropriate CellViewModel property for use in my view.
class ActionListViewModel: ObservableObject {
#Published var actionRepository: ActionStoreType
#Published var baseDateActionCellViewModels = [ActionCellViewModel]()
#Published var baseDateWeekActionCellViewModels = [ActionCellViewModel]()
#Published var previousActionCellViewModels = [ActionCellViewModel]()
#Published var futureActionCellViewModels = [ActionCellViewModel]()
#Published var baseDate: Date = Date()
#Published var hideCompleted: Bool = true
#Published var baseDateIsEndOfWeek: Bool = false
private var cancellables = Set<AnyCancellable>()
// MARK: Initializers
// Default initializer for production code.
init() {
self.actionRepository = ActionRepository()
self.baseDateIsEndOfWeek = isDateEndOfWeek(date: self.baseDate, weekEnd: self.baseDate.endOfWeekDate(weekStart: .sat))
loadPastActions()
loadBaseActions()
loadWeekTasks()
loadFutureActions()
}
// MARK: Functions for initializing the main groups of actions for the Homepage.
func isDateEndOfWeek(date currentDate: Date, weekEnd endOfWeekDate: Date) -> Bool {
if currentDate == endOfWeekDate {
print("Current Date: \(currentDate) and endOfWeekDate: \(endOfWeekDate) are the same!")
return true
} else {
print("The current date of \(currentDate) is not the end of the week (\(endOfWeekDate))")
return false
}
}
///The loadPastActions function takes the published actions list from the repository, and pulls a list of actions from before the base date. (It hides completed actions by default, but this is impacted by the viewModel's "hideCompleted" parameter.
///
///- returns: Assigns a list of actions from prior to the base date to the pastActionCellViewModels published property in the viewModel.
func loadPastActions() {
self.actionRepository.actionsPublisher.map { actions in
actions.filter { action in
action.beforeDate(self.baseDate) && action.showIfIncomplete(onlyIncomplete: self.hideCompleted)
}
.map { action in
ActionCellViewModel(action: action)
}
}
.assign(to: \.previousActionCellViewModels, on: self)
.store(in: &cancellables)
}
///The loadBaseActions function takes the published actions list from the repository, and pulls a list of actions from the base date. (It hides completed actions by default, but this is impacted by the viewModel's "hideCompleted" parameter.
///
///- returns: Assigns a list of actions from the base date to the viewModel's baseDateActionCellViewModels property.
func loadBaseActions() {
self.actionRepository.actionsPublisher.map { actions in
actions.filter { action in
action.inDateRange(from: self.baseDate, to: self.baseDate) && action.showIfIncomplete(onlyIncomplete: self.hideCompleted)
}
.map { action in
ActionCellViewModel(action: action)
}
}
.assign(to: \.baseDateActionCellViewModels, on: self)
.store(in: &cancellables)
}
/// The loadWeekActions takes the published actions list for the current user from the repository, and pulls a list of actions either from remainder of the current week (if not the end of the week), or from next week, if it's the last day of the week.
///
///- returns: Assigns a list of actions from the rest of this week or the next week to the viewModel's baseDateWeekActionCellViewModels property.
func loadWeekTasks() {
let startDate: Date = self.baseDate.tomorrowDate()
print("Start date is \(startDate) and the end of that week is \(startDate.endOfWeekDate(weekStart: .sat))")
self.actionRepository.actionsPublisher.map { actions in
actions.filter { action in
action.inDateRange(from: startDate, to: startDate.endOfWeekDate(weekStart: .sat)) && action.showIfIncomplete(onlyIncomplete: self.hideCompleted)
}
.map { action in
ActionCellViewModel(action: action)
}
}
.assign(to: \.baseDateWeekActionCellViewModels, on: self)
.store(in: &cancellables)
}
/// The loadFutureActions function takes the published actions list for the current user from the repository, and pulls a list of actions from after the week tasks.
///
///- returns: Assigns a list of actions from the future (beyond this week or next, depending on whether the baseDate is the end of the week) to the futureActionCellViewModels property in the viewModel.
func loadFutureActions() {
let startAfter: Date = baseDate.tomorrowDate().endOfWeekDate(weekStart: .sat)
self.actionRepository.actionsPublisher.map { actions in
actions.filter { action in
action.afterDate(startAfter) && action.showIfIncomplete(onlyIncomplete: self.hideCompleted)
}
.map { action in
ActionCellViewModel(action: action)
}
}
.assign(to: \.futureActionCellViewModels, on: self)
.store(in: &cancellables)
}

Use user location as a determinant in displaying content

I ask for user location and then extract their location so I can display it. This is how I extract their location:
func extractLocation(){
CLGeocoder().reverseGeocodeLocation(self.userLocation) { (res, err) in
guard let safeData = res else {return}
var address = ""
// address += safeData.first?.name ?? ""
// address += ", "
address += safeData.first?.locality ?? ""
self.userAddress = address
// print(address)
}
}
To display it, I just use #StateObject to refer to the class and then Text(LocationModel.userAddress)
In the app Im currently working, I want to display different content according to the location the users at. Information is stored in Cloud Firestore. As an example, lets have collection "FRIENDS". There, I have 3 auto ID:
name: Peter, location: New York
name: John, location: Los Angeles
name: James, location: Miami
So, if I'm in New York, only Peter's name will be displayed, if I'm in Los Angeles, only John's name will be displayed, and so on.
I thought that using
db.collection("FRIENDS").whereField("location", isEqualTo: LocationModel.userAddress).getDocuments ...
would work, but the app crashed. How would I solve this problem?
EDIT:
this the exact code im using:
struct Test: Identifiable{
var id: String = UUID().uuidString
var name: String
}
class UseLocationViewModel: NSObject,ObservableObject{
#StateObject var LocationModel = LocationViewModel()
let db = Firestore.firestore()
#Published var test = [Test]()
func useLocation(){
db.collection("FRIENDS").whereField("location", isEqualTo: LocationModel.userAddress).getDocuments { (querySnapshot, err) in
guard let documents = querySnapshot?.documents else {return }
self.test = documents.map { (queryDocumentSnapshot) -> Test in
let data = queryDocumentSnapshot.data()
let name = data["Name"] as? String ?? ""
return Test(name: name)
}
}
}
}
struct AnyView: View{
#StateObject var useLocationModel = UseLocationViewModel()
var body: some View{
VStack{
ForEach(useLocationModel.test){ info in
Text(info.name).foregroundColor(Color.white)
}
}.onAppear(){
useLocationModel.useLocation()
}
}
}
EDIT 2: if I use .whereField(LocationModel.userAddress, isEqualTo: "location" ) I get the following errors after launching:
RED ERROR Thread 1: "Invalid field path (). Paths must not be empty, begin with
'.', end with '.', or contain '..'"
PURPLE ERROR Accessing StateObject's object without being installed
on a View. This will create a new instance each time.
If I use it as in edit 1 (.whereField("location", isEqualTo: LocationModel.userAddress)) I only get purple error

Resources