Computed properties and performance issues

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.

Replies

How many transactions have you in the array ?

Have you evaluated how many times you run filter ?

You do not say how transactions are updated.

May be you could add Bool properties, to know that a transaction of a certain type has been added. And store the last computed vars spentNeedsBudget in lastNeeds…

Then you would call

    var spentNeedsBudget: Double {
       if newNeeds {
           lastNeeds =  transactions!.filter { $0.category == .needs && $0.amount < 0 }.reduce(0, { $0 + $1.amount }) }
           newNeeds = false
       } 
        return lastNeeds
}