Waves
A podcast client I have been building and rebuilding since 2020, with full CarPlay support, an App Clip, and a shared Swift package underneath it.
Overview
Waves is the podcast client I keep coming back to. It started in 2020 as a SwiftUI app built on a shared Swift package, grew a CarPlay scene, a widget, a share extension, and an App Clip over the following year, and is currently being rebuilt with a designer on modern Swift and the latest Apple frameworks.
It is the longest-running thing in this portfolio, and the only project here where I have owned every layer: the feed parsing, the player, the persistence model, the car experience, the marketing site, and the parts that did not work.
At a glance
Key features
- Full CarPlay support. A tabbed car interface with Up Next and Podcasts, artwork in list rows, live playback progress on the currently playing episode, and chapter navigation from the now playing screen.
- Chapter support. Podcast chapters parsed from the feed, listed, and seekable, in the app and in the car.
- App Clip. A lightweight entry point launched from the marketing site.
- Widget. A now playing widget sharing state with the app through an app group.
- Share extension. Feed URLs handed to the app from elsewhere in the system.
- Cross-device playback position. Listening progress synced through CloudKit, so an episode picked up on another device resumes where it left off.
Technical highlights
- PodKit. The models, feed types, an iTunes search client, a timed cache, and Combine HTTP helpers live in a separate Swift package with its own tests. The app, the CarPlay scene, and the widget all build against it. Factoring it out was originally about sharing code with the widget, and it ended up being the thing that made the CarPlay scene tractable, since the car interface could consume the same publishers the app did.
- CarPlay inside a constrained template API. CarPlay does not hand you views. You get a fixed set of templates and you compose within them. Playback progress on list rows comes from pushing updates into
CPListItemthrough a throttled Combine pipeline rather than redrawing, chapter navigation is the now playing screen's Up Next button repurposed because that is the affordance available, and the confirmation dialog for marking an episode played is a list template dressed as an alert, because CarPlay has no alert that fits. Most of the work was finding the legitimate use for each template that the design called for. - Image handling for the car. List artwork is resized to
CPListItem.maximumImageSizeand cached by key before it reaches a template, because loading full-resolution artwork per row while driving is not acceptable. - Combine-driven template regeneration. The car tabs are rebuilt from the same stores the app observes, so adding a podcast on the phone updates the car without a separate code path.
- A modern rebuild. The current pass, with a designer, brings the app onto modern Swift and the latest Apple frameworks, starting with a cleaner data layer that previews and tests can run against on their own while the app uses the real store.
Challenges
The honest answer is that cross-device progress sync is where this project stalled. Playback position is a bad fit for naive sync: it changes constantly, it changes on more than one device, and the correct resolution is not last-write-wins, because the device that wrote last is not necessarily the device you were actually listening on. Conflicts surfaced in TestFlight as episodes that jumped backwards, or resumed at a position from a session the listener had abandoned.
What that taught me is that the question was never which framework to use. It was that I had not decided what the source of truth for a playback position actually is, and no sync layer will decide that for you.
What I’d improve today
Settle the data model before touching sync again. Playback position wants to be an append-only log of listening sessions per device, reduced to a current position on read, rather than a single mutable field that several devices race to overwrite. That turns conflict resolution into a policy you can change rather than a bug you keep chasing.
The CarPlay layer is also worth revisiting as part of the rebuild. The Combine pipelines that feed the templates were written against the stores as they existed in 2021, and most of that plumbing would collapse into observation.
Repo status
Private, across a few repositories: the current rebuild, the earlier build that carries the CarPlay scene and App Clip, the shared Swift package, and the marketing site. Happy to walk through any of it.