Neraca is an iOS personal finance app built for speed, safety, and reliability. When handling personal finance tracking (wallets, budgets, debts, savings goals), two non-negotiable requirements emerge: absolute numerical precision and zero UI frame stuttering during database aggregates.
In this post, I detail how Neraca prevents floating-point rounding errors using Swift's native Decimal structures and how it aggregates transaction balances in the background using SwiftData's ModelActor concurrency architecture.
1. Eliminating Floating-Point Precision Drift
Using floating-point types (like Double or Float) for financial calculations is a dangerous anti-pattern. Because of binary representation constraints, simple operations can drift:
// The problem with Double
let walletBalance: Double = 0.1
let transaction: Double = 0.2
print(walletBalance + transaction) // Outputs: 0.30000000000000004
While a discrepancy of 0.00000000000000004 seems negligible, compounding these differences across thousands of transactions results in distorted balance ledgers.
To solve this, Neraca uses Swift's decimal floating-point arithmetic library (Decimal) across its entire storage and UI layer. Decimal utilizes base-10 representations, eliminating floating-point binary rounding artifacts.
@Model
final class Transaction {
@Attribute(.unique) var id: UUID
var title: String
var amount: Decimal // High-precision decimal type
var date: Date
var type: TransactionType // .income or .expense
init(title: String, amount: Decimal, date: Date, type: TransactionType) {
self.id = UUID()
self.title = title
self.amount = amount
self.date = date
self.type = type
}
}
2. Offloading Aggregations to SwiftData ModelActors
Aggregating spending totals by categories or calculating net worth requires summing transaction histories. Doing this calculations in-memory on the main thread triggers screen stutters (drops below 60 FPS) when a user has logged thousands of entries over several years.
SwiftData solves thread isolation by exposing the ModelActor protocol. In Neraca, I designed a thread-isolated background actor to process database calculations asynchronously:
import Foundation
import SwiftData
@globalActor
actor DatabaseQueryActor {
static let shared = DatabaseQueryActor()
}
@DatabaseQueryActor
actor FinanceAggregator: ModelActor {
nonisolated let modelContainer: ModelContainer
nonisolated let modelExecutor: any ModelExecutor
init(container: ModelContainer) {
self.modelContainer = container
let context = ModelContext(container)
context.autosaveEnabled = false
self.modelExecutor = DefaultSerialModelExecutor(modelContext: context)
}
/// Calculate sum of all transactions within a category
func sumCategory(categoryId: UUID, from startDate: Date, to endDate: Date) -> Decimal {
let context = modelContext
let predicate = #Predicate { transaction in
transaction.date >= startDate &&
transaction.date <= endDate
}
let descriptor = FetchDescriptor(predicate: predicate)
guard let transactions = try? context.fetch(descriptor) else { return 0 }
// Sum using high-precision Decimal reduction
return transactions.reduce(Decimal(0)) { total, transaction in
total + transaction.amount
}
}
}
3. Reactive UI Syncing
To integrate these background aggregations cleanly into SwiftUI layouts, Neraca uses the async/await pattern to fetch data from the aggregator actor whenever the transaction store signals a change:
struct BudgetProgressView: View {
@Environment(\.modelContext) private var context
@State private var totalSpent: Decimal = 0
var categoryId: UUID
var body: some View {
VStack(alignment: .leading) {
Text("Category Spending")
Text(totalSpent.formatted(.currency(code: "USD")))
.font(.title2)
.bold()
}
.task(id: categoryId) {
await recalculateBudget()
}
}
private func recalculateBudget() async {
let container = context.container
let aggregator = FinanceAggregator(container: container)
let sum = await aggregator.sumCategory(
categoryId: categoryId,
from: Date().startOfMonth(),
to: Date()
)
// Update UI on main thread
await MainActor.run {
self.totalSpent = sum
}
}
}
Summary
By designing Neraca to rely entirely on Decimal arithmetic and offloading complex calculations to thread-isolated SwiftData actors, the app remains fast and accurate. This architecture keeps UI operations smooth while ensuring financial data remains consistent.