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

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)
}

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 ?? []) {
...

Completion handler for Firebase Realtime DB lookup when one function needs the value of a previous function

I have the following code that fetches a schedule
func fetchSchedule(completion: #escaping () -> ()) {
scheduleRef.queryOrderedByValue().queryEqual(toValue: true).observe(.value, with: { snapshot in
self.schedule = []
if snapshot.value is NSNull {
// Null
} else {
for child in snapshot.children {
if let snapshot = child as? DataSnapshot,
let schedule = Schedule(snapshot: snapshot) {
self.schedule.append(schedule)
}
}
}
})
}
The above get the current schedule but what I am unclear on is that i need that value to then call the next function call which get the associated games for that schedule on the .onAppear() of the view in SwiftUI
func getGames() {
scheduleStore.fetchSchedule()
//
gameStore.fetchGames(weekId: self.scheduleStore.schedule[0].weekId)
}
the gameStore.fetchGames always returns null, likely because it has not finished processing the fetchSchedule function?
How do I ensure the first function finishes before it calls the fetchGames?
You have a completion handler built into your function signature on fetchSchedule, but you aren't using it.
func fetchSchedule(completion: #escaping () -> ()) {
scheduleRef.queryOrderedByValue().queryEqual(toValue: true).observe(.value, with: { snapshot in
self.schedule = []
if snapshot.value is NSNull {
// Null
} else {
for child in snapshot.children {
if let snapshot = child as? DataSnapshot,
let schedule = Schedule(snapshot: snapshot) {
self.schedule.append(schedule)
}
}
completion() //<-- Here
}
})
}
Then,
func getGames() {
scheduleStore.fetchSchedule(completion: {
gameStore.fetchGames(weekId: self.scheduleStore.schedule[0].weekId)
})
}
You're not showing all of your code, but you may also have something broken between self.schedule, which you set in fetchSchedule, and self.scheduleStore, you you send to fetchGames -- make sure you've only got one place you're storing data -- should it be self.schedule in both places?
Update, based on comments
This code is approximate, since I don't have access to your types, but it should get you started:
func fetchSchedule(completion: #escaping ([Schedule]) -> ()) {
scheduleRef.queryOrderedByValue().queryEqual(toValue: true).observe(.value, with: { snapshot in
if snapshot.value is NSNull {
// Null
} else {
let schedules = snapshot.children.compactMap { child in
if let snapshot = child as? DataSnapshot, let schedule = Schedule(snapshot: snapshot) {
return schedule
}
return nil
}
completion(schedules)
}
})
}
func getGames() {
scheduleStore.fetchSchedule { schedules in
gameStore.fetchGames(weekId: schedules[0].weekId)
}
}

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)

SwiftUI Fetching contacts duplication

I have a model view that has fetching contacts function:
class ContactsStore: ObservableObject {
#Published var contacts = [CNContact]()
func fetch() {} ...
And then in my View:
#EnvironmentObject var store: ContactsStore
var groupedContacts: [String: [CNContact]] {
.init (
grouping: store.contacts,
by: {$0.nameFirstLetter}
)
}
...
List() {
ForEach(self.groupedContacts.keys.sorted(), id: \.self) { key in ...
I've shortened my code for the convenience, will add/edit if needed. The issue I faced - every time my View is rendered the fetch function is called and my array of contacts is duplicated in my view. TIA for a help
UPD: Duplication happens due to method calling fetch in the List .onAppear. So I'm looking at how to call this method only once and not every time the View appears.
You can do fetch in Init() like that:
struct someView: View{
var list: [Settings]
init(){
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
let fetchRequest: NSFetchRequest<Settings> = Settings.fetchRequest()
//... some order and filter if you need
self.list = context.fetch(fetchRequest)
}
var body: some View{
...
ForEach(list){settings in
...
}
...
}
}
Didn't try it with grouping, but you asked how to fetch only once. The answer is - in init().
But you cant get the #Environment in init, so i get the context from AppDelegate. Or you can pass the context as init parameter

Resources