Hello everyone,
I need your help with a performance issue I’m encountering in my app.
I’m learning app development in SwiftUI, and I built a simple budgeting app based on the 50/30/20 rule, which consists in dividing your expenses in “Needs”’, “Wants”, and “Savings and debts”. The main objects of my app are months, and transactions, each month containing an array of associated transactions.
My app shows graphs for the current month. For example, it can show a pie chart representing the expenses of different categories.
Now, in order to create these graphs, I have to compute some numbers from the month. For example, I have to retrieve the percent of the budget that has been spent. For now, I did this by adding various computed variables to my MonthBudget model, which is the model containing the transactions. This looks something like this:
@Model
final class MonthBudget {
@Relationship(deleteRule: .cascade) var transactions: [Transaction]? = []
let identifier: UUID = UUID()
var monthlyBudget: Double = 0
var needsBudgetRepartition: Double = 50
var wantsBudgetRepartition: Double = 30
var savingsDebtsBudgetRepartition: Double = 20
// Definition of other variables...
}
extension MonthBudget {
@Transient
var totalSpent: Double {
spentNeedsBudget + spentWantsBudget + spentSavingsDebtsBudget
}
@Transient
var remaining: Double {
totalAvailableFunds + totalSpent
}
// Negative amount spent for a specific category (ex: -250)
@Transient
var spentNeedsBudget: Double { transactions!.filter { $0.category == .needs && $0.amount < 0 }.reduce(0, { $0 + $1.amount }) }
@Transient
var spentWantsBudget: Double { transactions!.filter { $0.category == .wants && $0.amount < 0 }.reduce(0, { $0 + $1.amount }) }
@Transient
var spentSavingsDebtsBudget: Double { transactions!.filter { $0.category == .savingsDebts && $0.amount < 0 }.reduce(0, { $0 + $1.amount }) }
// Definition of multiple other computed properties...
}
Now, this approach worked fine when I only had one or two months with a few transactions in memory. But now that I’m actually using the app, I see serious performance issues (most notably hangs/freezes) whenever I am trying to display a graph.
I used “Instruments” to inspect what was going wrong with my app, and I saw that the hangs happened mostly when trying to get the value of these variables, meaning that the actual computing was taking too long.
I’m therefore trying to find a more efficient way of getting these informations (totalSpent, spentNeedsBudget, etc.). Is there a common practice that would help with these performance issues?
I thought about caching the last computed property (and persist it using SwiftData), and using a function that would re-compute and persist all of these properties whenever a transaction is added or removed. But this has multiple cons:
- I’d have to call the function that re-computes the properties and stores them in memory each time I delete/add a transaction, losing the very benefit of using computed properties
- This would potentially be a bad idea: if there’s some sort of bug with SwiftData and one or multiple transactions are added or deleted without the user actually doing anything, there could be a mismatch between the persisted amount/value and the actual value.
Did any of you face the same issue as me, and if so, how did you solve it?
Any idea is appreciated!
Thanks for reading so far,
Louis.