A lightweight SwiftUI app for modeling investment scenarios and tracking capital allocations. Built with a pragmatic MVVM-style architecture and file-based persistence so you can iterate quickly without backend dependencies.
Status: Working baseline with Overview, Scenarios, and Capital tabs. Actively evolving.
- Overview tab — at-a-glance summary of the currently selected Scenario plus quick actions.
- Scenarios tab — create, duplicate, select, and delete financial scenarios.
- Capital tab — (existing screen) manage contributions, allocations, and portfolio summaries.
- Persistence — scenarios saved to
Documents/scenarios.json(no database required). - SwiftUI-first — single source of truth via
AppViewModelas an@EnvironmentObject.
- AppViewModel (
ObservableObject,@MainActor) is the top-level source of truth.- Publishes
scenarios, aselectedID, and computedoutputs(fromCalculator). - Persists state to disk on change and recomputes outputs.
- Publishes
- Views
MainTabViewhosts tabs: Overview, Scenarios, Capital.OverviewViewreads fromAppViewModeland provides quick actions.CapitalView(and itsCapitalViewModel) contains capital-specific UI and logic.
- Calculator encapsulates the financial math and produces
CalculatorOutputperScenario.
Data Flow
Scenario edits → AppViewModel.scenarios → save() → recompute via Calculator → outputs
- Xcode 15 or newer (Swift 5.9+)
- iOS 17.0+ (SwiftUI / Charts usage may require these SDKs)
- No backend services required
If you use an older Xcode, you may need to relax the iOS deployment target or remove newer API usages.
-
Clone the repo
git clone https://github.com/therealtplum/SnowbirdCalc.git cd SnowbirdCalc -
Open
SnowbirdCalc.xcodeproj(or.xcworkspaceif present) in Xcode. -
Build & Run on iOS Simulator (iPhone 15/16) or a device.
The app will create and load Documents/scenarios.json on first run and seed a default scenario.
SnowbirdCalc/
├─ AppViewModel.swift # Source of truth: scenarios, selection, persistence, outputs
├─ MainTabView.swift # Tabs: Overview, Scenarios, Capital
├─ OverviewView.swift # Overview dashboard (uses AppViewModel)
├─ CapitalView.swift # Capital tab UI
├─ CapitalViewModel.swift # Capital tab logic
├─ Calculator.swift # Financial computation → CalculatorOutput
├─ AboutView.swift # (Optional) About screen
├─ BunnyEggView.swift # (Optional) Fun/testing view
└─ ChartsSection.swift # (Optional) Shared charts/sections helpers
If you maintain your own
SectionCardcomponent, keep a single definition to avoid duplicate type errors.
- Root injection (in your
@mainapp):@main struct SnowbirdApp: App { @StateObject private var appVM = AppViewModel() var body: some Scene { WindowGroup { MainTabView() .environmentObject(appVM) } } }
- In child views, use:
@EnvironmentObject var vm: AppViewModel
AppViewModelsaves on each mutation toDocuments/scenarios.json.- Safe to edit scenarios via
updateCurrent { … }; outputs recompute automatically.
- Keep
maingreen. Do feature work on branches:git switch -c feature/overview-polish # work, commit git push -u origin feature/overview-polish - Commit messages: imperative mood, scope first (e.g.,
overview: add snapshot card & quick actions).
- Performance chart (value vs. return %) on Overview.
- Donut allocation breakdown shared with Capital tab.
- Capital calls & upcoming items surfaced on Overview.
- Export/import scenarios (JSON).
- Unit tests for
Calculatorand edge cases.
Crash: “No ObservableObject of type AppViewModel found.”
You forgot to inject .environmentObject(appVM) at the app root or in Previews.
“Missing argument for parameter 'isPresented' in call.”
When using .sheet, .popover, or .alert, provide a Binding<Bool>:
@State private var show = false
.sheet(isPresented: $show) { MyView() }Blank Overview / recursion
Avoid nesting MainTabView inside itself:
// ❌ Wrong
NavigationStack { MainTabView() }
// ✅ Right
NavigationStack { OverviewView() }Duplicate type 'SectionCard'
Ensure only one SectionCard exists in the target, or mark a file-local helper as private and remove the other.
MIT © Thomas Plummer