Filter Firestore data

Hello!

I am working on an Expense Tracker app where a user inputs their expense & the data is stored in Firestore.

I have one page where I display all payments & I display the data by fetching all from Firestore. And this query works without an issue.

    func getAllPayments() {
        
        db.collection("payments")
            .order(by: "paymentDate", descending: true)
            .addSnapshotListener { querySnapshot, error in
            guard let documents = querySnapshot?.documents else {
                print("No documents")
                return
            }
                
            self.payments = documents.compactMap { (queryDocumentSnapshot) -> Payment? in
                return try? queryDocumentSnapshot.data(as: Payment.self)

            }
        }
    }

But I also want to display some Swift Charts with filtered data based on current day, current month, current year. Have been searching around but completely stumped! I feel like I need another function using the one above to filter the full amount of data? But just canā€™t get to it!

My model, I have a paymentDate, which stores the date:

struct Payment: Hashable, Identifiable, Codable {
    
    var id:String? = UUID().uuidString
    var paymentName:String
    var paymentAmount:Double
    var paymentCurrency:String
    var paymentCategory:String
    var paymentPlan:String
    var paymentDate:Date
}

The chart page that I am using for Month (& have a pretty identical one for Day). At the bottom Iā€™m currenty calling the getAllPayments function, which is why I am getting all payments but just not sure how to filter it down to the right dates.

struct MonthlySpend: View {
    
    @EnvironmentObject var paymentModel:PaymentModel
    var primaryColor = "Primary"
    var secondaryColor = "Secondary"
    var secondaryText = "SecondaryText"
    
    var body: some View {
        
        ZStack {
            PopoutCard()
                .frame(height:300)
            VStack {
                HStack {
                    Text("MONTH")
                        .font(.system(size: 18, weight: .bold))
                    
                    Spacer()
                    VStack (alignment: .trailing) {
                        Text("$500")
                            .foregroundColor(Color(primaryColor))
                            .font(.system(size: 18, weight: .bold))
                        Text("SPENT")
                            .font(.system(size: 12))
                    }
                }
                .padding()
                Chart {
                    RuleMark(y: .value("Average", 380))
                        .foregroundStyle(Color(primaryColor))
                        .lineStyle(StrokeStyle(lineWidth: 1, dash: [5]))
                        .annotation(alignment: .leading) {
                            Text("Avg.")
                                .font(.caption)
                                .foregroundColor(Color(secondaryText))
                        }
                    ForEach(paymentModel.payments, id: \.paymentDate) { payment in
                        BarMark(
                            x: .value("month", payment.paymentDate, unit: .month),
                            
                            y: .value("Amount",  payment.paymentAmount)
                        )
                    }
                }
                .frame(height: 180)
                .padding()

                .chartXAxis {
                    AxisMarks() { date in
                        AxisValueLabel()
                    }
                }
            }
            .onAppear() {
                self.paymentModel.getAllPayments()
            }
        }
    }
}

Hey Lloyd,

Thatā€™s interesting.

So basically, first you get (and listen to) all documents from your ā€œpaymentsā€ collection in Firestore, ordered by paymentDate.

Then your goal is to chart the data for the current day, current month, and current year.

You could filter things on the front-end like this:

// get date for 1 month ago
let monthAgo = Calendar.current.date(byAdding: .month, value: -1, to: Date())

// filter payments array for paymentDate more recent than 1 month ago
currentMonthPayments = paymentModel.payments.filter({$0.paymentDate < monthAgo})

and then chart that new array that only includes relevant data for current month. Do the same thing for current day and current year.

Alternatively, you could include that filter in your query.

let query = db.collection("payments")
            .whereField("paymentDate", isLowerThan: monthAgo)
            .order(by: "paymentDate", descending: true)

Plus, you didnā€™t set a limit to your query and this is a root collection. Do you expect to download many documents from this collection every time your app inits ? For an MVP or a passion app thatā€™s fine (Firestore allows for 50k free reads per day), but for thousands of users bills will start adding up.

Just fyi, thereā€™s a catch about those Firestore queries. Say, you want to query not for current month, but only last month - a period between 2 months ago and 1 month ago - thatā€™s trickier. The documentation says:

ā€œYou canā€™t have an equality(==) and inequality (<, <=, >, and !=) on the same field across disjunctions (or, in, array-contains-any)ā€
Source: Executar consultas simples e compostas no Cloud Firestore Ā |Ā  Firebase

So anything more complex than current date, current month, current year will pose new also interesting challenges.

My guess is in the case of more complex filtering in FIrestore, it would be simpler to do all that in Swift rather than asking Firestore to do that.

@LloydH

There is a series on CWC+ named " Case Study: How to Build an App [Paused]" in which Chris builds an App that is a bit like a task scheduler. In that there is some great date manipulation code examples where, in this particular case, the App displays a week in which you may have tasks and also allows you to step forward a week and back a week. Whilst that might not suit your particular needs, the techniques in that might give you a clue how to achieve what you need.

Hi! Sorry I didnā€™t respond to this sooner, I went away for a couple of weeks so Iā€™ve only just picked the problem back up.

Thanks so much for your help! Iā€™ve managed to fiddle with what you have done & got something working. Iā€™ve created a filter above the chart then added that into the loop. It seems to work well for day, month & year. Trying to have the month chart to display every day in the month along the axis next!

ā€˜ā€™ā€™
let currentMonth = Calendar.current.component(.month, from: Date())
let filteredPayments = paymentModel.payments.filter { payment in
let paymentMonth = Calendar.current.component(.month, from: payment.paymentDate)
return paymentMonth == currentMonth
}

                ForEach(filteredPayments, id: \.paymentDate) { payment in
                    BarMark(
                        x: .value("month",  payment.paymentDate, unit: .weekOfMonth),
                        
                        y: .value("Amount",  payment.paymentAmount)
                    )

ā€˜ā€™ā€™

Thanks for the note on the document downloads - at the moment itā€™s just a passion project / practice as Iā€™m still quite new to Swift. If it did have 1000s of users whatā€™s the best practice there?

Thanks!

1 Like

Thatā€™s awesome Lloyd!

Well, it depends! But I assume each user is tracking their own expenses, so maybe each user should have their own subcollection of expenses? It really depends on the features you want to build.

But this is definitely a great passion project to learn Firebase and Swift! I recommend going through the ChatApp in CWC+ if you have the time. Itā€™s not directly linked to your use case, but personally it really helped me understand Firestore architecture and all that stuff