A UX decision I got wrong on the first try
This one started as a small missing-feature bug: tapping a Discover release card did nothing. The fix itself ended up simple, but I built the wrong interaction first, and that’s the more interesting part.
Problem
Found this while device-verifying a different bug fix: the Discover tab’s release cards were pure display, no tap handler, no add action at all. Checked the code and the card’s own doc comment gave it away - it literally said the add-to-playlist action was supposed to ship in a previous issue, alongside the “Not Interested” swipe gesture. Only half of that ever got built.
Why this approach (attempt one)
The app already had a working service method that does exactly this - add a catalog album to a playlist, no adaptation needed since Discover’s feed items are already the right type. Wiring it up was the easy part. The interesting design question was the interaction: how does the user actually trigger it?
My first instinct was consistency with the existing “Not Interested” gesture already on the same card: add a second swipe action, opposite edge, green tint, plus icon. It matched the row’s existing interaction model, it required zero new view hierarchy, and it worked - I built it, wrote tests, installed it on a physical device, and confirmed it working end to end.
Gotchas
Then I actually lived with it for a bit, and it was wrong. Everywhere else in the app - Playlist, History - the equivalent action works the same way: tap a card, get a detail sheet with buttons. Discover was now the one tab where you had to know to swipe instead. I’d optimized for “smallest diff on this one card” and missed the more important consistency: matching how the rest of the app already handles this exact interaction.
That’s a real lesson about scoping a UI decision from the wrong reference point. I compared the new action against its sibling on the same card instead of against its counterpart in other tabs. Both are legitimate “matches existing patterns” arguments, but only one of them survives actually using the app - and the way to find out was to use it, not to argue from the diff.
Implementation (attempt two)
Rebuilt it properly: a new detail sheet and view model, deliberately mirroring the two that already exist for Playlist and History almost line for line - same header layout (artwork, title, artist, badge, year), same AddState enum (idle/adding/done/failed) driving the button’s label, icon, and tint, same .sensoryFeedback wiring for haptic confirmation the instant the state changes. Discover’s version is simpler than its siblings since it doesn’t need a same-artist-suggestions section - the whole Discover feed already is the suggestions.
One incidental find while doing this: History’s existing add-to-playlist code and my new Discover code needed identical logic for resolving the linked playlist and mapping a failure to actionable copy (“link a playlist in Settings,” “this playlist isn’t yours to edit,” and so on). Rather than copy-paste it a second time, I pulled it into a small shared file both features now call into. Small thing, but it’s the kind of duplication that’s easy to justify skipping on a two-line diff and much harder to justify once you’re looking at the same block appearing twice.
Results
Full suite green at 171 tests, including new coverage for the release-detail view model’s three real branches (successful add, no-playlist-linked failure, dismiss-and-close). Rebuilt and reinstalled on a physical device a second time and confirmed the actual interaction matches Playlist and History exactly.
The whole detour cost maybe twenty extra minutes. Worth it - shipping the wrong interaction pattern into a shared UI surface is a lot more expensive to notice and undo later than catching it during a device pass on day one.
Related reading
When the same icon means two different things, and why the fix wasn't just a label swap
Two thumbs-down buttons, same red styling - one permanent, one harmless. The visual language was making a promise the code wasn't keeping, and the method name was part of the bug.
Unifying "Dismissed" vs "Not Interested," and the Settings row that repeated its own section header
Four names for one concept, and the discovery that the obvious fix would have introduced a new inconsistency one level up - plus the one repetition that's a safety feature, not a bug.
The setting that worked perfectly and felt completely broken
A filter default that only applied on relaunch, because a TabView keeps view models alive and state seeded at construction is frozen. The fix deleted a concept.