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.