A native Swift app (iOS, macOS) for keeping track of what I've read, watched, played, and experienced. The vision is a personal knowledge base with full editing capabilities, synced via Contentful.
./run bootstrap # first time (before shell function exists)
source ~/.zshrc # activate completionsBootstrap handles all one-time setup:
- Installs Brewfile dependencies (
brew bundle: 1Password CLI, swift-format) - Authenticates with 1Password (prompts if needed)
- Writes
Saga/Config/Config.xcconfigwith Contentful credentials from thesagavault (CONTENTFUL_SPACE_IDandCONTENTFUL_ACCESS_TOKENitems) - Adds shell completions to your
.zshrcfor theruncommand
After setup, use run (with tab completion) instead of ./run. Re-run bootstrap any time to refresh credentials.
Saga follows an MVVM architecture with SwiftUI views:
- Features/: Domain-specific features (Books, Assets, Content)
- Each feature contains Models, Views, ViewModels, and Services
- Shared/: Reusable components, extensions, and utilities
- Services/: Core services like persistence (Core Data) and syncing (Contentful)
- Navigation is managed via
NavigationHistorywith a home-based sidebar layout
Swift-based scripts live in scripts/Sources/ and are run via the run wrapper at the repo root. Use run --help to see available scripts, or run <script> --help for script-specific options.
| Script | Description |
|---|---|
app |
Build, launch, or run UI tests |
bootstrap |
Set up shell completions and pull secrets from 1Password |
drop-bot-commits |
Drop version bump commits on branch and rebase onto main |
checks |
Run Swift format and/or lint checks |
version-and-release |
Manage version tags and Github releases |
Builds the macOS app in Debug mode using xcodebuild and launches the resulting app, or you can use Xcode.
run app # build quietly and launch
run app --verbose # show full xcodebuild output
run app --ui-test # run XCUITest UI tests + screenshots
run app --help # show usageUI tests run with XCUITest via xcodebuild test (xctest runner) and export screenshots to
build/UITestScreenshots/<branchHash>/.
macOS requires granting Accessibility and Screen Recording permissions to the test runner
(Xcode or xcodebuild). These permissions cannot be limited to a single target app, but the
tests only interact with Saga and capture Saga's window content.
Hot reload is enabled via the Inject library. To use it:
- Install InjectionIII from the Mac App Store or build from source
- Run the app in Xcode (Debug build)
- Launch InjectionIII and select the Saga project folder when prompted
- Edit any Swift file and save - the UI will automatically refresh
Hot reload is applied universally via .hotReloadable() at the WindowGroup level, so all views automatically get hot reload support without individual modifications. No per-view setup required.
All Swift code is formatted and linted using swift-format with the repo config in .swift-format.
run checks # format and lint
run checks --format # format only
run checks --lint # lint only
run checks --help # show usageCI runs this on every PR and fails if formatting produces any changes.
When adding a new Core Data model:
- Create the entity in
Saga.xcdatamodeld - Create the corresponding Swift model file
- Add the model to
PersistenceModelenum - Ensure Xcode isn't generating auto-gen files for the entity
GitHub Actions automatically bump versions on PRs and create releases on merge:
- PRs include release type checkboxes (Major/Minor/Patch, default Patch) and a
# What changed?section for release notes - On PR open/edit, the workflow reads the base version from
main, computes the next version, sets the build number, and commits the bump - On merge, it creates a GitHub Release using the
# What changed?text as release notes
If there are multiple branches, each with their own version bump, then one merges, there will be a
conflict in project.pbxproj. To fix, run drop-bot-commits to remove bot-authored version bumps
and rebase onto origin/main. Force pushing the branch should then recreate a new version bump
commit and you're good.
Shouldn't be necessary, but you can run version-and-release for manual version management (see --help for subcommands).