PodHaven is a modern podcast application for iOS, built with Swift and SwiftUI. It provides a clean and intuitive interface for discovering, subscribing to, and listening to your favorite podcasts.
Website: artisanalsoftware.com/podhaven
- Discover & Search: Find new podcasts powered by the iTunes Search API.
- Trending Podcasts: Browse top podcasts across 18 genre categories using Apple's rankings.
- Subscribe & Manage: Easily subscribe to your favorite podcasts and manage your library.
- Tags: Organize podcasts and episodes with custom tags.
- Episode Playback: A modern audio player with background playback, lock screen controls, and playback position tracking.
- Playback Queue: Manage a queue of upcoming episodes.
- Download for Offline: Save episodes to your device to listen without an internet connection.
- OPML Import/Export: Import your existing podcast subscriptions from another app, or export your library from PodHaven.
- Share Extension: Add new podcasts directly from Safari or other apps using the share sheet.
- Widgets: Home screen widgets for quick access to your podcasts.
- New Episode Notifications: Get notified when subscribed podcasts publish new episodes, with per-podcast control and rich artwork attachments.
- Background Refresh: Automatic feed updates in the background.
- macOS 27 or later with Xcode 27 or later
- Swift 6.4 or later
- Clone the repository:
git clone https://github.com/jubishop/podhaven.git
- Navigate to the project directory:
cd podhaven - Set up repository tooling:
bin/setup bin/check --full
- Open the project in Xcode:
open PodHaven.xcodeproj
- Build the project:
Press
Cmd+Rin Xcode to build and run the app in the simulator.
For more advanced users, here are the commands to build and test from the command line.
For repository knowledge and tooling, use bin/check --documents-only after
a batch of Markdown edits and bin/check for fast static checks. Run
bin/check --full after setup or foundation changes, and before a tooling PR
or release. These commands do not build the Swift app. Use focused Swift
checks during development. Before any push or merge into main, and before
a release, run bin/test-all locally for the final revision. It runs the full
app suite on My Mac, macro tests, and automated tooling and skill tests, and
rejects skipped tests and Swift warnings. GitHub does not run test CI. See the
validation policy
and test workflow.
Click to expand Build & Test Commands
env -u SDKROOT xcodebuild build-for-testing -hideShellScriptEnvironment -project PodHaven.xcodeproj -scheme PodHaven -destination 'platform=macOS,name=My Mac' LM_FORCE_LINK_GENERATION=YESRun the complete local validation from the repository root:
bin/test-allLogs, the Xcode result bundle, and the tested checkout are recorded under
.cache/test-all/. Grant Accessibility access to the terminal or app running
tests in System Settings → Privacy & Security → Accessibility. The local test
wrapper uses a native accessibility client to expose SwiftUI controls on My Mac.
Swift Testing filters must stay at suite level; method-level filters can report success while running zero tests.
bin/with-test-accessibility xcodebuild test -hideShellScriptEnvironment -project PodHaven.xcodeproj -scheme PodHaven -destination 'platform=macOS,name=My Mac' -only-testing:PodHavenTests/SomeSuite -resultBundlePath .cache/FocusedTests.xcresult LM_FORCE_LINK_GENERATION=YES > .cache/focused-tests.log 2>&1
bin/check-swift-results .cache/FocusedTests.xcresult --build-log .cache/focused-tests.logUse a new result-bundle path for each run. Hosted UI tests need the wrapper;
running them directly with Cmd+U does not start its accessibility client.
Print the current app version:
bin/versionSet the next app version across all targets and build configurations:
bin/version 1.0.1This changes the app version (MARKETING_VERSION). The command rejects invalid
or older versions. Use bin/version --help for usage.
Commit your changes, then test, archive, and upload a build:
bin/shipitTo also distribute that build to the external Everyone group, supply TestFlight "What to Test" notes:
bin/shipit --notes "Improved playback reliability."With --notes, the command checks the existing external Everyone group before
building. After upload, it checks processing every 30 seconds for up to 30 minutes,
adds the notes, submits for beta review when needed, and verifies the group
assignment. Testers are notified when Apple makes the build available. Apple beta
review can remain pending after the command finishes. Notes must be nonblank and
at most 4000 bytes.
If processing times out or distribution fails after upload, rerun the same command
from the same clean commit. It reuses the uploaded build. You can also use
--notes after an upload-only run of that commit. A single local upload receipt
in Git's metadata directory supports retries; each new upload replaces it.
bin/shipit increments the build number automatically. If Apple closes a version
for external beta review, use bin/version to set a higher app version, commit it,
and upload a new build. Successful runs also publish the Git tag and GitHub
release and mirror the branch and tag to SourceHut. The existing generated GitHub
release summary remains separate from the supplied TestFlight notes.
Other options:
| Option | Behavior |
|---|---|
-h, --help |
Show usage without building or uploading. |
-f, --force |
Allow a branch other than main. A clean working tree is still required. |
--api-key PATH --api-key-id ID --api-issuer-id ID |
Authenticate with an App Store Connect API key. Supply all three options together. |
The API key options can also be set with ASC_KEY_PATH, ASC_KEY_ID, and
ASC_ISSUER_ID. Keep private keys outside the repository. Without a key, uploads
use Xcode's Apple ID login. Distribution uses Fastlane's own Apple ID login and
may request two-factor authentication; FASTLANE_USER selects the Apple ID.
An App Manager or Admin role is needed for external distribution. Fastlane is
required only with --notes; install it with brew install fastlane.
bin/shipit and bin/deploy.sh are aliases and accept the same options.
Release the next minor version with notes from the latest TestFlight build:
bin/appstoreThe command requires clean main. It increments the minor version (2.1.2
becomes 2.2), commits and pushes the version change, tests, archives, uploads,
waits for that exact build to finish processing, and submits for App Review.
Apple releases it automatically after approval.
Override the release number or public "What's New" notes when needed:
bin/appstore --notes "Improved playback reliability."
bin/appstore --release 3 --notes "A new major release."--release must be strictly greater than the current version and have zero or
one dot. Without --notes, the command copies notes from the latest iOS build
with a two-dot TestFlight version. It uses the app's primary language or en-US
and stops if the latest build has no usable notes. Public notes must contain
1 to 4000 characters and are used for every existing listing language.
Show the live version, pending versions and reviews, and uploaded builds for the current local app version without releasing:
bin/appstore --statusTo submit an exact existing build, use --release VERSION --build NUMBER.
This skips the version change and upload. The build must match that version.
The description, screenshots, and other listing metadata carry over. Only the build, release setting, and "What's New" text are updated. The command verifies Apple's saved build, notes, release setting, and submission state before reporting success. It does not wait for Apple to complete App Review.
The build must be App Store eligible with export compliance already complete. Apple can still require changes to listing or review information before accepting a submission; the command reports those errors.
If a run stops, use bin/appstore --status to inspect the state, then retry the
same command. It retains the chosen version, notes, and exact build across
retries, including after a failed version push. See
Versioning and releases for the complete workflow and retry rules.
Fastlane is required (brew install fastlane). Authentication uses the same
--api-key, --api-key-id, and --api-issuer-id options and ASC_* environment
variables described above. Without a key, it uses Fastlane's Apple ID login.
Use bin/appstore --help for all options.
PodHaven is built using modern Swift practices and a clean, modular architecture.
- UI Layer: Built entirely with SwiftUI for a declarative and responsive user interface.
- State Management: Uses Swift's Observation framework (
@Observable) with Factory for dependency injection. - Database: Uses GRDB.swift for fast and safe access to the local SQLite database, preferring the QueryInterface API over raw SQL.
- Networking: Leverages URLSession with the iTunes Search API for podcast discovery, and XMLCoder for parsing RSS feeds.
- Image Handling: Nuke for efficient image loading and caching.
- Concurrency: Built from the ground up with Swift's modern structured concurrency (
async/await). - Macros: Custom Swift macros for
ReadableErrorconformance and state management. - Error Handling: Structured approach using
ReadableErrorprotocol andErrorKit, with Sentry integration for production error reporting. - Logging: Centralized logging via Apple's
OSLogwith structured file logging and Sentry forwarding.
| Target | Description |
|---|---|
| PodHaven | Main iOS app |
| PodhavenShare | Share extension for adding podcasts from other apps |
| PodHavenWidget | Home screen widget extension |
| PodHavenMacros | Swift macro compilation target |
| PodHavenTests | Parallelized test suite |
| Package | Purpose |
|---|---|
| GRDB.swift | SQLite database management |
| XMLCoder | RSS feed parsing |
| Nuke | Image loading and caching |
| Factory | Dependency injection |
| Tagged | Type-safe identifiers |
| Sentry | Error reporting and crash analytics |
| IdentifiedCollections | Collection utilities |
| OrderedCollections | Foundation collection extensions |
| Semaphore | Concurrency utilities |
| swift-sharing | Shared state management |
| swift-log | Structured logging |
| swift-navigation | Navigation utilities |
| swift-algorithms | Algorithm utilities |
Tests use the Swift Testing framework with the @Suite / #expect DSL. The test plan runs all suites in parallel. An in-memory GRDB database (AppDB.inMemory()) powers repository tests, and Factory overrides with .context(.test) provide test fakes.
Contributions are welcome! If you have a feature request, bug report, or want to contribute to the code, please feel free to open an issue or submit a pull request.
This project is licensed under a Source Available License. You are free to view, study, and contribute to the code, but commercial use, redistribution, and derivative works (outside of contributions) are not permitted. See the LICENSE file for full details.
Copyright (c) 2026 Justin Bishop. All rights reserved.