- Xcode 15+
- XcodeGen
- Swift 5.9 / SwiftUI — macOS 14.0+, menu-bar-only (
LSUIElement: true) - XcodeGen —
project.ymlis the source of truth - No external dependencies beyond Sparkle for auto-updates
xcodegen generate
xcodebuild -project Yield.xcodeproj -scheme Yield -configuration Debug buildOr open Yield.xcodeproj in Xcode and hit Cmd+B.
Yield/
├── YieldApp.swift # @main, MenuBarExtra, AppState
├── Models/
│ ├── ProjectStatus.swift # Per-project state and status
│ ├── HarvestModels.swift # Harvest API types
│ └── ForecastModels.swift # Forecast API types
├── Services/
│ ├── APIClient.swift # REST client (Bearer auth, snake_case)
│ ├── HarvestService.swift # Harvest API
│ ├── ForecastService.swift # Forecast API
│ ├── OAuthService.swift # OAuth 2.0 with local callback server
│ ├── KeychainHelper.swift # Secure token storage
│ └── DateHelpers.swift # Week bounds and date formatting
├── ViewModels/
│ └── TimeComparisonViewModel.swift # Fetch, merge, sort, timer management
└── Views/
├── MenuBarContentView.swift # Main dropdown UI
├── ProjectRowView.swift # Project row with progress bar
├── SettingsView.swift # OAuth + PAT settings
└── StatusIndicator.swift # Color-coded status dot
- Version source of truth:
project.yml(MARKETING_VERSION/CURRENT_PROJECT_VERSION) - Auth: OAuth (default, Keychain storage) or Personal Access Token (advanced)
- Time format:
h:mmeverywhere (e.g.,3:30not3.5h) - State:
@Observable— no Combine orObservableObject - Releases: Signed with Developer ID, notarized by Apple, distributed via GitHub Releases with Sparkle appcast
- Bump
MARKETING_VERSIONandCURRENT_PROJECT_VERSIONinproject.yml xcodegen generate- Build a release archive:
xcodebuild -project Yield.xcodeproj -scheme Yield -configuration Release archive -archivePath build/Yield.xcarchive - Re-sign Sparkle binaries and the app with Developer ID +
--timestamp - Zip:
ditto -c -k --keepParent Yield.app build/Yield-X.Y.Z.zip - Notarize:
xcrun notarytool submit build/Yield-X.Y.Z.zip --apple-id <email> --team-id <team> --password <app-specific-password> --wait - Staple:
xcrun stapler staple Yield.appthen re-zip - Sign for Sparkle:
sign_update build/Yield-X.Y.Z.zip - Add entry to
appcast.xmlwith the signature and length - Commit, push, create GitHub release with the zip attached