A personal finance tracking iOS app built as a school project. The primary goal is learning and applying Clean Architecture with a UseCase pattern, local persistence via SwiftData, and remote storage via Firebase.
| Component | Technology |
|---|---|
| Language | Swift 6 |
| UI | SwiftUI (iOS 18+) |
| Observability | @Observable (not ObservableObject) |
| Local persistence | SwiftData |
| Remote backend | Firebase Auth + Firestore |
| Dependency injection | DIContainer |
| Navigation | AppCoordinator |
The project follows a strict Clean Architecture with clear layer separation:
Domain
└── Entities (Transaction, Account, Institution, User)
└── UseCases (AddTransaction, GetAccounts, ArchiveAccount, ...)
└── Protocols (-Providing)
Data
└── Local (SwiftData — -LocalSource, -Model)
└── Remote (Firebase — -RemoteSource, -Remote)
└── Storings (-Storing, implements -Providing)
Presentation
└── ViewModels (@Observable, @MainActor)
└── Views (SwiftUI)
└── Subviews (SplitRowView, AccountRowView, ...)
App
└── DIContainer (single source of truth for DI)
└── AppCoordinator (navigation + ViewModel ownership)
| Pattern | Example |
|---|---|
| Protocol | TransactionProviding |
| Implementation | TransactionStoring |
| Local source | TransactionLocalSource |
| Remote source | TransactionRemoteSource |
| Remote struct | TransactionRemote |
| UseCase input | AddTransactionInput |
- User — application user
- Institution — financial institution (bank, insurance, etc.)
- Account — bank account linked to an institution, archivable
- Transaction — financial operation with amount distribution across multiple accounts via
TransactionSplit
- Account balances are computed on demand (not stored)
- Accounts cannot be deleted — they are archived (
isArchived: true) - Transactions referencing an archived account remain consistent
GetAccountssupports anAccountFilter(.active,.archived,.all)
- Transaction list with swipe actions (delete, edit)
- Add and edit transaction with multi-account split
- Transaction detail view
- Account list grouped by institution
- Add and edit account
- Account archiving
- Inline institution creation from the account form
- Full local persistence via SwiftData
- SwiftUI previews with seeded data (
PreviewHelpers,PreviewData) - Typed error handling per domain (
TransactionError,AccountError,InstitutionError)
- Firebase save (offline-first) +
SyncManagerwith timestamp-based sync - Firebase authentication (email/password)
- Dashboard with balances and recent transactions
- Archived accounts view with unarchive support
- iPad layout support
- Account detail view
- Date picker in
TransactionFormView - Global error handling and alerts
EchoLedger/
├── App/
│ ├── EchoLedgerApp.swift
│ ├── AppDelegate.swift
│ ├── DIContainer.swift
│ ├── DIContainer+ViewModels.swift
│ └── Navigation/
│ └── AppCoordinator.swift
├── Features/
│ ├── Transaction/
│ │ ├── Domain/
│ │ │ ├── Transaction.swift
│ │ │ ├── TransactionSplit.swift
│ │ │ ├── TransactionCategory.swift
│ │ │ └── TransactionError.swift
│ │ ├── Data/
│ │ │ ├── TransactionModel.swift
│ │ │ ├── TransactionLocalSource.swift
│ │ │ └── TransactionStoring.swift
│ │ └── Presentation/
│ │ ├── TransactionListView.swift
│ │ ├── TransactionListViewModel.swift
│ │ ├── TransactionDetailView.swift
│ │ ├── TransactionFormView.swift
│ │ ├── TransactionFormViewModel.swift
│ │ └── Subviews/
│ │ ├── SplitRowView.swift
│ │ └── AccountPickerView.swift
│ ├── Account/
│ │ ├── Domain/
│ │ │ ├── Account.swift
│ │ │ ├── AccountCategory.swift
│ │ │ ├── AccountFilter.swift
│ │ │ └── AccountError.swift
│ │ ├── Data/
│ │ │ ├── AccountModel.swift
│ │ │ ├── AccountLocalSource.swift
│ │ │ └── AccountStoring.swift
│ │ └── Presentation/
│ │ ├── AccountListView.swift
│ │ ├── AccountListViewModel.swift
│ │ ├── AccountFormView.swift
│ │ ├── AccountFormViewModel.swift
│ │ └── Subviews/
│ │ └── AccountRowView.swift
│ └── Institution/
│ ├── Domain/
│ │ ├── Institution.swift
│ │ ├── InstitutionType.swift
│ │ └── InstitutionError.swift
│ ├── Data/
│ │ ├── InstitutionModel.swift
│ │ ├── InstitutionLocalSource.swift
│ │ └── InstitutionStoring.swift
│ └── Presentation/
│ └── Subviews/
│ └── AddInstitutionFormView.swift
├── Shared/
│ ├── Extensions/
│ │ ├── Double+Euro.swift
│ │ └── String+Double.swift
│ └── Preview/
│ ├── PreviewData.swift
│ └── PreviewHelpers.swift
- Clone the repository
- Open
EchoLedger.xcodeproj - Add your
GoogleService-Info.plist(Firebase) - Build & Run on an iOS 18+ simulator
Julien Cotte — Academic project 2026