Inspiration

I went looking for what I actually pay for every month and could not answer the question. Not because the information was hidden, but because it was scattered across an App Store receipt, a bank statement, a gym membership I signed on paper, and three trials I said yes to at some point in 2024.

Every subscription tracker I tried solved the easy half of that. It showed me a number. None of them helped with the hard half, which is leaving. Because leaving is the part the services actively engineer against: the cancel button four screens deep, the retention call you cannot skip, the minimum term nobody mentioned at signup, the free trial that quietly renewed at full price.

So the app is called Cancelled, and the thesis is in the name. A tracker that only counts is a scoreboard for a game you are losing. The number is not the product. The exit is.

What it does

Cancelled ships a cancellation catalog: 167 services, each one carrying more than a link.

  • Where you actually cancel, and whether that URL reaches a real cancel action at all. 141 do. Eleven cannot be finished by any link, because the service demands a phone call, an email, a letter, or that you turn up in person. For those, the app leads with the written steps instead of a button that goes nowhere.
  • What the service will do to stop you. Twenty three are flagged as fighting back, and each obstacle is weighted by what it costs you:

$$ F(s) \;=\; \sum_{i \,\in\, O(s)} w_i, \qquad w_i \in {1,2,3,4} $$

where turning up in person or posting a written notice scores 4, a retention call or an early termination fee scores 3, and a service is marked as one that will resist when $F(s) \ge 3$. That threshold decides whether you get a warning before you start, so a retention queue never catches you cold.

Getting your list in takes seconds: scan a receipt, paste an App Store screenshot, drop in a Notion page, or point it at a spreadsheet. Everything parses on device.

Then the app watches the renewals and draws the next two weeks as weather. A day is clear, rain, or storm, tiered on the money leaving that day:

$$ \text{tier}(d)= \begin{cases} \text{clear} & t_d = 0\[2pt] \text{rain} & 0 < t_d < 25\[2pt] \text{storm} & t_d \ge 25 \end{cases} $$

That one threshold lives in a single shared type, because the forecast calendar, the Live Activity and the widget all tell the same story and used to disagree about whether a day was heavy.

The score at the top of the screen is not what you spend. It is what you kept.

How I built it

One SwiftUI and SwiftData codebase, 99 Swift files and about 21,600 lines, shipping to four platforms natively:

  • iOS and iPadOS 17, the home of the app
  • macOS 14, a real Mac app rather than Catalyst, with its own sidebar layout, Settings scene and menu commands
  • watchOS 10, with complications and a renewal alert you can answer from your wrist
  • Widgets, Lock Screen, and a Live Activity counting down to the next charge

Platform differences hide behind a small shim layer and no operation view helpers, so feature views stay platform neutral instead of growing a thicket of conditionals.

The widget and the watch never touch SwiftData. The phone owns the store and hands out one flat snapshot through an app group, which is also what makes the watch app possible: the watch has no database, only the snapshot and three requests it can send back, mark paid, cancel, delete.

RevenueCat runs the paywall and the free tier gate. PostHog handles analytics. Rive drives McCoin, the mascot who gets visibly drained by the subscriptions you forgot about. App Intents expose subscriptions to Siri, Spotlight and Shortcuts. The whole app ships in 26 languages.

Challenges

Watch Connectivity lies quietly. Between paired simulators, transferFile and phone to watch transferUserInfo are both accepted and then silently never delivered. No error, no callback, nothing. Everything the phone sends, snapshot and brand logos alike, had to ride in the application context. And the logos still arrived blank, because the CDN serves WebP, which iOS decodes and watchOS does not. They are transcoded to PNG before they are sent.

Xcode only merges a String Catalog when you build in the IDE. Plain xcodebuild stops at emitting per file string data and never runs the merge, which means a command line build silently produces a catalog missing every new key. That became a script that builds each target and then runs the merge itself.

Translation broke in ways English never shows. A translation with two arguments and no positional specifiers reorders in several languages, and the second argument lands in the first slot, so a string gets passed where an integer belongs and the app crashes in Arabic only. The verifier now rejects that shape outright. Directional SF Symbols also had to become forward and backward rather than left and right, because only those mirror in right to left layouts.

Motion has a hard ceiling outside the app. Rive can never run in a widget, a Live Activity or a complication, because WidgetKit bans the view type it needs. Every bit of personality out there had to be rebuilt in pure SwiftUI shapes and gradients.

You cannot actually cancel for someone on iOS. This was the real design constraint. Apple billed subscriptions can be cancelled without leaving the app through StoreKit, but for everything else the honest ceiling is the shortest possible path plus a truthful warning about what waits at the end of it. Rather than fake the rest, the friction map became the feature.

What I learned

That the interesting part of a money app is almost never the arithmetic. It is the correctness around the edges: currency and dates through format styles rather than string building, plural rules that need more than the two forms English gets by with, category values kept as English identifiers in storage and translated only on the way to the screen.

And that a genuinely useful feature can be mostly research. The catalog is not clever code. It is 167 services someone had to sit down and actually check.

What is next

FinanceKit, so the app can spot recurring charges in Apple Card and Apple Cash transactions instead of asking you to remember them. Repairing the recurrence math in locales whose default calendar is not Gregorian, where the billing date currently drifts. And growing the catalog past 167, region by region, because the local gym is always the worst offender.

Built With

  • brandfetch
  • rive
  • swiftui
  • uikit
Share this project:

Updates