Skip to content
Development

A UX decision I got wrong on the first try

By Victor Da Luz
iosswiftuxdev-logdeep-cut-atlas

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