A multi-platform (iOS + CLI) app that shows the live BTC/USD exchange price, refreshed every second. Built with the Essential Developer method — TDD, small always-green commits, modular design, and CI. One framework-free domain core, reused verbatim by two thin composition roots (an iOS app and a CLI).
Layered Swift packages under Modules/ (one package each), consumed by two roots:
| Layer | Package | Role |
|---|---|---|
| Domain | BTCPriceFoundation |
BTCPrice value type + BTCPriceLoader seam. Foundation-only — no UI or networking framework. |
| Infrastructure | BTCPriceAPI |
HTTPClientProtocol seam + URLSessionHTTPClient adapter, Binance/Coinbase endpoints + mappers, RemoteBTCPriceLoader. |
| Orchestration | BTCPriceComposition |
Fallback composite (Binance → Coinbase) + 1-second timeout decorator. |
| Presentation | BTCPricePresentation |
MVVM TickerViewModel emitting AsyncStream<TickerViewState> (Foundation-only). |
| Root (iOS) | BTCPriceApp |
SwiftUI TickerView + composition root. |
| Root (CLI) | btc-cli |
Console renderer + composition root. |
Every dependency points down to the framework-free domain core; only the two roots know
concrete types. See docs/ for the light/dark diagrams.
Story: A customer wants to watch the live BTC/USD price
Narrative #1 — Happy path
As an online customer
I want the app to show the latest BTC/USD price, refreshed every second
So I always see the current market price
Given the customer has connectivity
When the app (iOS) or CLI is running
Then it displays the latest BTC/USD price, updating every second
Narrative #2 — Update failure
As a customer
I want to keep seeing the last known price and be told the update failed
So I understand the value may be stale
Given the customer has already seen a price
When an update fails OR takes longer than one second
Then it keeps showing the last known price and its timestamp
And shows "Failed to update value. Displaying last updated value from <date>"
And hides that error as soon as an update succeeds again
- Primary:
https://api.binance.com/api/v3/ticker/price?symbol=BTCUSDT - Fallback:
https://api.coinbase.com/v2/prices/BTC-USD/spot(keyless; replaces the spec'd CryptoCompare endpoint, which now requires an API key)
iOS app (Xcode)
git clone https://github.com/anthony1810/BTCPrice.git && cd BTCPriceopen BTCPriceApp/BTCPriceApp.xcodeproj— the committed project; no generation needed.- Select an iOS 17+ simulator and press Run (⌘R).
The
.xcodeprojis generated fromBTCPriceApp/project.ymlwith XcodeGen and committed as the source of truth for reviewers. After editing the spec, regenerate withcd BTCPriceApp && xcodegen generate(brew install xcodegen).
CLI (btc-cli)
open btc-cli/Package.swift— Xcode opens it as a Swift package.- Choose the btc-cli scheme and press Run — or from a terminal:
swift run --package-path btc-cli(Ctrl-C to stop).
All packages in one window — open BTCPrice.xcworkspace for a single Xcode window with a scheme
per package (app, CLI, each module, the E2E package). Use My Mac as the destination for the
module/CLI/E2E schemes; only BTCPriceApp runs on an iOS simulator.
Tests — run a package's suite with swift test --package-path <path>, e.g.
swift test --package-path Modules/BTCPriceAPI or swift test --package-path btc-cli. The live
end-to-end tests are a separate package: swift test --package-path BTCPriceAPIEndToEndTests
(needs an unrestricted network — api.binance.com geo-blocks some regions, including CI IPs).
.github/workflows/test-modules.yml— on every push/PR, runs each module'sswift testand thebtc-clitests in a matrix on Linux (officialswiftcontainer). Running off Apple platforms actively proves the framework-free core is platform-agnostic. Deterministic — no network, no simulator..github/workflows/test-ios.yml— runsxcodebuild testfor the iOS app on a simulator (macOS) with the Thread Sanitizer enabled.- The live Binance/Coinbase end-to-end tests live in their own package (
BTCPriceAPIEndToEndTests/) and are not on the PR gate:api.binance.comreturns HTTP 451 to CI's US-based IPs, so no hosted runner can reach it (and a public repo shouldn't self-host). Run them locally, or on-demand via theCI-Modulesworkflow's manuale2ejob — where the Binance test auto-skips (CIenv) and the Coinbase test runs.
MIT.