Skip to content
Development

Pro-gated iCloud sync for the "Not Interested" list

By Victor Da Luz
iosswifticlouddev-logdeep-cut-atlas

Deep Cut Atlas’s Pro tier promises that dismissing an album in “Not Interested” follows you across devices and survives a reinstall. I built that this week, and the interesting parts were the ones I didn’t plan for.

The decision that made this easy

A design spike a few weeks back already ruled out the obvious approach. Flipping SwiftData’s ModelConfiguration(cloudKitDatabase:) to sync everything through CloudKit sounds simple, but it can’t be toggled at runtime, and on iOS 17.0-17.1 it syncs even when you tell it not to. Since Pro-gating means free users must never touch iCloud, that ruled it out entirely.

The dismissal list itself is small, additive, and low-stakes. Worst case if a sync hiccups: one already-seen album reappears. That’s exactly the shape iCloud’s key-value store handles well, so the plan was a single JSON blob under one KVS key, with a union-merge that keeps local SwiftData as the source of truth and only layers sync on top.

Reusing more than I expected

I went in expecting to write a good amount of new plumbing. Turned out an earlier feature (syncing which playlists the app created) had already built the exact seam I needed: a small protocol wrapping NSUbiquitousKeyValueStore, plus an in-memory test double. I didn’t touch either. I just built a new service on top of the same abstraction.

The one piece union-merge doesn’t handle on its own is deletes. If you undo a dismissal on your phone, and your iPad’s copy of the dataset still has it, a plain union brings it right back. The fix is tombstones: instead of removing a key from the synced set, you mark it deleted with a timestamp, and reconciliation compares timestamps to decide whether a delete or a re-add wins.

To keep every call site from needing to know about Pro status, sync, and iCloud, I wrapped the existing local store in a decorator that implements the exact same interface. Free users get the plain store, untouched. Pro users get the same store with a thin sync layer wrapped around it. One function decides which, in one place.

The crash that took down the wrong tests

Writing tests for the reconcile logic, I hit a wall: the whole test run started failing. Not just my new tests, dozens of unrelated ones across the codebase, all reporting a suspicious uniform 0.000 second duration. My first read was that I’d broken something systemic. I hadn’t.

What actually happened: one of my tests crashed the shared test process outright, and everything still queued behind it in that process got marked “failed” without ever running. The real crash was buried under a pile of collateral damage.

The bug itself was almost embarrassing once I found it. A test helper built a SwiftData container, then returned only its context, not the container itself. Nothing was left holding the container alive after the function returned, so the moment a later test touched that context, it was operating on a torn-down store. Signal trap, no useful message.

The annoying part is I’d already been warned. An earlier test file in this exact codebase has a comment about this precise failure mode. I just didn’t connect it when writing a new helper for a new test file. Lesson filed away properly this time, in a knowledge-base note instead of a comment I might not see next time.

Proving it without a second device

The real test for “does this sync via iCloud” is two devices on the same account. I only had one handy. Testing locally would normally prove nothing, since a dismissal already persists locally regardless of whether sync works at all.

The trick was breaking that assumption on purpose. I dismissed an album while Pro was active, gave iCloud a little time to push, then deleted the app entirely, wiping local storage along with it. Reinstalled, relaunched, checked whether that album was already excluded from the feed without me touching it again. It was. Since local storage had nothing to say about it, the only way that album could still be marked dismissed was if it came back down from iCloud on launch. That’s about as close to proof as a single device gets.

Where it stands

The push and pull paths both work, confirmed the hard way rather than assumed. What’s still unverified is real-time propagation between two live devices, since I didn’t have a second one on hand this round. That’s a five-minute check whenever an iPad or spare phone is available, not a design gap.

Related reading