Skip to main content

Architecture

Thrust follows modern iOS architecture patterns with SwiftUI and SwiftData.

Overview

┌─────────────────────────────────────────────────────────┐
│                      SwiftUI Views                       │
│  (Declarative UI, State-driven, Reactive)               │
└─────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────┐
│                    View Models                           │
│  (@Observable, Business Logic, State Management)        │
└─────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────┐
│                      Managers                            │
│  (Singleton Services, Coordinators, Utilities)          │
└─────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────┐
│                       Engines                            │
│  (Analytics, Forecasting, Calculations)                 │
└─────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────┐
│                    SwiftData Models                      │
│  (@Model, Persistence, Relationships)                   │
└─────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────┐
│                  Data Layer                              │
│  (SwiftData, CloudKit, Keychain, UserDefaults)          │
└─────────────────────────────────────────────────────────┘

Core Principles

1. SwiftUI-First

Everything is SwiftUI:
  • No UIKit (except for specific APIs)
  • Declarative UI
  • State-driven updates
  • Composition over inheritance
Example:
struct TransactionRow: View {
    let transaction: Transaction
    
    var body: some View {
        HStack {
            AppleStyleIconView(...)
            VStack(alignment: .leading) {
                Text(transaction.title)
                Text(transaction.date, style: .date)
            }
            Spacer()
            Text(transaction.amount, format: .currency(...))
        }
    }
}

2. SwiftData for Persistence

Why SwiftData:
  • Native Swift API
  • Type-safe queries
  • Automatic migrations
  • CloudKit sync support
  • Better than Core Data
Model Example:
@Model
final class Transaction {
    var id: UUID
    var amount: Decimal
    var date: Date
    var title: String
    
    @Relationship(deleteRule: .nullify)
    var account: Account?
    
    @Relationship(deleteRule: .nullify)
    var category: Category?
    
    init(amount: Decimal, date: Date, title: String) {
        self.id = UUID()
        self.amount = amount
        self.date = date
        self.title = title
    }
}
Querying:
@Query(
    filter: #Predicate<Transaction> { transaction in
        transaction.date >= startDate && transaction.date <= endDate
    },
    sort: \Transaction.date,
    order: .reverse
)
var transactions: [Transaction]

3. Observable Pattern

@Observable for State:
  • Replaces ObservableObject
  • Automatic change tracking
  • Better performance
  • Cleaner syntax
Manager Example:
@Observable
final class NotificationManager {
    static let shared = NotificationManager()
    
    var hasPermission: Bool = false
    var pendingNotifications: [Notification] = []
    
    func requestPermission() async {
        // Request logic
        hasPermission = true
    }
}
Usage in View:
struct SettingsView: View {
    @Environment(NotificationManager.self) private var notificationManager
    
    var body: some View {
        Toggle("Notifications", isOn: $notificationManager.hasPermission)
    }
}

4. Cross-Screen Data Consistency

When multiple screens display the same computed data (e.g., budget suggestions shown both on Dashboard capsule and Budgets screen), use a shared @Observable manager injected via .environment() to guarantee a single source of truth. Pattern: Shared Manager
@MainActor @Observable
final class BudgetSuggestionManager {
    private(set) var allSuggestions: [BudgetSuggestion] = []
    private(set) var isLoaded = false

    var topSuggestion: BudgetSuggestion? { activeSuggestions.first }

    func rebuild(modelContext: ModelContext, budgets: [Budget], ...) {
        let transactions = fetchTransactions(modelContext: modelContext)
        allSuggestions = BudgetSuggestionEngine.buildSuggestions(transactions: transactions, ...)
        isLoaded = true
    }
}
Why not just call the Engine directly? Engines are stateless — each call site may pass different input data (e.g., different transaction fetch limits), producing different results. The shared manager owns both the fetch and the computation, ensuring all screens see the same output. Invalidation: Screens that modify related data (e.g., BudgetListView on budget/currency change) call manager.rebuild(). Screens that only read (e.g., InsightCapsulesView) do a fallback rebuild if isLoaded == false.

5. Separation of Concerns

Clear responsibilities: Views:
  • UI only
  • No business logic
  • Minimal state
  • Composition
View Models:
  • Business logic
  • State management
  • Data transformation
  • Validation
Managers:
  • Shared services
  • System APIs
  • External integrations
  • Singleton pattern
Engines:
  • Pure functions
  • Calculations
  • Analytics
  • No state

Data Flow

Unidirectional Data Flow

User Action → View → View Model → Manager/Engine → Model → SwiftData
                ↑                                              ↓
                └──────────────── Update ←─────────────────────┘
Example Flow:
  1. User taps “Add Transaction”
  2. View shows sheet
  3. User fills form
  4. View calls ViewModel method
  5. ViewModel validates data
  6. ViewModel creates Transaction model
  7. SwiftData saves to database
  8. @Query automatically updates
  9. View re-renders with new data

State Management

Three types of state: 1. View State (Local)
@State private var isExpanded = false
@State private var selectedTab = 0
2. Shared State (Environment)
@Environment(\.modelContext) private var modelContext
@Environment(NotificationManager.self) private var notificationManager
3. Persistent State (SwiftData)
@Query var transactions: [Transaction]

Layers

1. Presentation Layer

Views:
  • SwiftUI views
  • Minimal logic
  • Composition
  • Reusable components
View Models:
  • @Observable classes
  • Business logic
  • State management
  • Data transformation
Example:
@Observable
final class DashboardViewModel {
    var totalBalance: Decimal = 0
    var safeToSpend: Decimal = 0
    var isLoading = false
    
    func refresh(accounts: [Account], budgets: [Budget]) {
        isLoading = true
        
        // Calculate totals
        totalBalance = accounts.reduce(0) { $0 + $1.balance }
        
        // Calculate safe to spend
        let engine = SafeToSpendEngine()
        safeToSpend = engine.calculate(accounts: accounts, budgets: budgets)
        
        isLoading = false
    }
}

2. Business Logic Layer

Managers:
  • Singleton services
  • System integrations
  • Shared functionality
Engines:
  • Pure calculation functions
  • Analytics
  • Forecasting
  • No side effects
Example Engine:
struct SafeToSpendEngine {
    func calculate(
        accounts: [Account],
        budgets: [Budget],
        recurringPayments: [RecurringPayment] = []
    ) -> Decimal {
        let totalBalance = accounts.reduce(0) { $0 + $1.balance }
        let budgetAllocated = budgets.reduce(0) { $0 + $1.remaining }
        let upcomingPayments = recurringPayments
            .filter { $0.nextDate <= Date().addingTimeInterval(7 * 24 * 60 * 60) }
            .reduce(0) { $0 + $1.amount }
        
        return totalBalance - budgetAllocated - upcomingPayments
    }
}

3. Data Layer

SwiftData:
  • Models with @Model
  • Relationships
  • Queries with @Query
  • Migrations
CloudKit (Optional):
  • Sync across devices
  • Backup
  • Family sharing
Keychain:
  • Sensitive data
  • API keys
  • Tokens
UserDefaults:
  • App settings
  • Preferences
  • Non-sensitive data

Key Patterns

1. Coordinator Pattern

SheetCoordinator:
  • Centralized sheet management
  • Type-safe navigation
  • Deep linking support
@Observable
final class SheetCoordinator {
    var activeSheet: SheetType?
    
    enum SheetType: Identifiable {
        case addTransaction
        case editAccount(Account)
        case budgetDetail(Budget)
        
        var id: String {
            switch self {
            case .addTransaction: return "addTransaction"
            case .editAccount(let account): return "editAccount-\(account.id)"
            case .budgetDetail(let budget): return "budgetDetail-\(budget.id)"
            }
        }
    }
    
    func present(_ sheet: SheetType) {
        activeSheet = sheet
    }
    
    func dismiss() {
        activeSheet = nil
    }
}

2. Repository Pattern

Not used directly - SwiftData @Query replaces repositories Instead of:
class TransactionRepository {
    func fetchAll() -> [Transaction] { ... }
}
We use:
@Query var transactions: [Transaction]

3. Factory Pattern

For complex object creation:
struct AccountFactory {
    static func createCash(name: String, balance: Decimal) -> Account {
        Account(
            name: name,
            type: .cash,
            balance: balance,
            currency: .USD
        )
    }
    
    static func createCreditCard(
        name: String,
        balance: Decimal,
        creditLimit: Decimal
    ) -> Account {
        Account(
            name: name,
            type: .creditCard,
            balance: balance,
            creditLimit: creditLimit,
            currency: .USD
        )
    }
}

4. Strategy Pattern

For different calculation strategies:
protocol BudgetStrategy {
    func calculate(transactions: [Transaction]) -> Decimal
}

struct MonthlyBudgetStrategy: BudgetStrategy {
    func calculate(transactions: [Transaction]) -> Decimal {
        // Monthly calculation
    }
}

struct WeeklyBudgetStrategy: BudgetStrategy {
    func calculate(transactions: [Transaction]) -> Decimal {
        // Weekly calculation
    }
}

Privacy Architecture

Ghost Mode (Default)

All data on-device:
  • No network requests
  • No cloud sync
  • Maximum privacy
  • Fully functional
Implementation:
@Observable
final class PrivacyManager {
    static let shared = PrivacyManager()
    
    var mode: PrivacyMode = .ghost
    
    enum PrivacyMode {
        case ghost      // All local
        case connected  // With sync
    }
    
    var canMakeNetworkRequests: Bool {
        mode == .connected
    }
}

Connected Mode (Optional)

With user consent:
  • iCloud sync
  • Bank integration (EU)
  • Real-time prices
  • AI categorization
Gated features:
if privacyManager.canMakeNetworkRequests {
    // Fetch stock prices
    await stocksService.fetchPrices()
} else {
    // Use cached data
    return cachedPrices
}

Performance

Optimization Strategies

1. Lazy Loading:
@Query(
    filter: #Predicate<Transaction> { $0.date >= startDate },
    sort: \Transaction.date,
    order: .reverse
)
var transactions: [Transaction]

// Only loads visible transactions
List(transactions) { transaction in
    TransactionRow(transaction: transaction)
}
2. Pagination:
@State private var limit = 50
@State private var offset = 0

@Query(
    sort: \Transaction.date,
    order: .reverse
)
var allTransactions: [Transaction]

var paginatedTransactions: [Transaction] {
    Array(allTransactions.prefix(limit))
}
3. Background Processing:
Task.detached(priority: .background) {
    let result = await heavyCalculation()
    await MainActor.run {
        self.result = result
    }
}
4. Caching:
@Observable
final class CacheManager {
    private var cache: [String: Any] = [:]
    
    func get<T>(_ key: String) -> T? {
        cache[key] as? T
    }
    
    func set<T>(_ key: String, value: T) {
        cache[key] = value
    }
}

Testing Architecture

Unit Tests

Test engines and managers:
final class SafeToSpendEngineTests: XCTestCase {
    func testCalculation() {
        let engine = SafeToSpendEngine()
        let accounts = [
            Account(name: "Cash", balance: 1000)
        ]
        let budgets = [
            Budget(name: "Food", limit: 500)
        ]
        
        let result = engine.calculate(accounts: accounts, budgets: budgets)
        
        XCTAssertEqual(result, 500)
    }
}

UI Tests

Test critical flows:
final class TransactionFlowUITests: XCTestCase {
    func testAddTransaction() {
        let app = XCUIApplication()
        app.launch()
        
        app.buttons["Add Transaction"].tap()
        app.textFields["Amount"].typeText("50")
        app.textFields["Title"].typeText("Coffee")
        app.buttons["Save"].tap()
        
        XCTAssertTrue(app.staticTexts["Coffee"].exists)
    }
}

Security

Data Protection

Encryption:
  • SwiftData encrypted at rest
  • Keychain for sensitive data
  • CloudKit encrypted in transit
Authentication:
  • Face ID / Touch ID
  • Passcode fallback
  • Auto-lock
Implementation:
@Observable
final class SecurityManager {
    func authenticate() async -> Bool {
        let context = LAContext()
        var error: NSError?
        
        guard context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) else {
            return false
        }
        
        do {
            return try await context.evaluatePolicy(
                .deviceOwnerAuthenticationWithBiometrics,
                localizedReason: "Unlock Thrust"
            )
        } catch {
            return false
        }
    }
}

Scalability

Handling Large Datasets

Strategies:
  1. Pagination - Load data in chunks
  2. Indexing - SwiftData indexes for fast queries
  3. Lazy loading - Only load what’s visible
  4. Background processing - Heavy calculations off main thread
  5. Caching - Cache expensive calculations
Example:
// Efficient query with predicate
@Query(
    filter: #Predicate<Transaction> { transaction in
        transaction.date >= startDate &&
        transaction.date <= endDate &&
        transaction.amount > 0
    },
    sort: \Transaction.date,
    order: .reverse
)
var transactions: [Transaction]

Next Steps

Project Structure

Navigate the codebase

API Reference

Detailed API documentation

Contributing

Contribute to Thrust

Quick Start

Get started developing