theme rss
visitor@fireatwill:~/projects$ ↑ history · enter to run
~/projects/0x0102
fireatwill ¬ 0x0102 ¬ projects2026.07

Letting Go

author Sofus Bech status 1.0 · testflight prep stack swift · swiftui · cloudkit · ios 16 repo private site lettinggo.fireatwill.org

A calm, editorial iOS app that eases newcomers into minimalism — shipped under the working title Minimalist, grown into Letting Go 1.0. Four jobs in one place: run the 30-day Minimalism Game, keep the 16 rules close, decide what to keep through a guided 90/90 → 20/20 flow, and cool off an impulse buy with a 30/30 wait timer. Now synced through iCloud and playable as a shared game — one clock for everyone.

swiftswiftuicloudkitios

Most minimalism advice is a wall of text. The good stuff — the 30-day game, the 20/20 rule, the pause-before-you-buy trick — is simple to say and hard to keep in front of you at the moment you need it. I wanted the practice in my pocket, not on a shelf: a few honest tools, no ads, no upsell, nothing tracking me.

The problem

The ideas that actually change behaviour are tiny rituals. "Can I replace this for under 20 dollars in under 20 minutes? Then let it go." "Wait 30 days on anything over 30 dollars." They work — but only if something nudges you at the register or during the daily declutter. A book can't do that. An app can, if it stays calm instead of gamifying you into another attention trap.

Eliminate the unnecessary so the necessary may speak — including in the interface.

How it works

It's a single-screen SwiftUI app with a shared state object and a plain Screen enum for a router — no navigation stack, no backend, no third-party dependencies. State lives in one place and every let-go action flows through one seam, so Home, Stats and Settings can never disagree about the count. The keep/let-go verdict is derived from the three questions you answer, never stored — so it can't drift:

AppModel.swift
// the verdict is computed from the three answers — never saved,
// so it can never fall out of sync with what you actually ticked
var verdict: Verdict {
    if usedInLast90Days { return .keep }        // the 90/90 rule
    if cheapAndQuickToReplace { return .letGo }  // the 20/20 rule
    return .undecided
}

Data persists to a JSON file in Application Support — saves are debounced and written off the main thread. Decoding is migration-safe: every field falls back to a seed default, and an unreadable file is quarantined rather than silently overwriting your history. Since 1.0 there's a second layer on top: an iCloud store that decorates the local file rather than replacing it, so disk stays the source of truth and everything keeps working offline:

CloudStore.swift
// Layer 2: iCloud sync. Decorates LocalFileStore — disk stays the
// source of truth and everything keeps working offline; the CloudKit
// private database mirrors it as a single record (whole JSON blob).
// Conflict policy: newest savedAt wins, whole-blob.
final class CloudKitStore: PersistenceStore { ... }

The whole thing still ships as two targets from one source of truth — a signed Xcode build and a Swift Playgrounds app anyone can run without a developer account (sync-playground.sh --check fails the push on drift). The playground has no iCloud entitlement, so it simply keeps the local store; the cloud layer compiles there but is never constructed.

Letting Go 1.0 — what changed

The working title Minimalist became Letting Go, and the fakes became real. The seeded friends and standings are gone — the 30-Day Game is now a shared round over CloudKit: invite a friend, everyone plays against one clock, late joiners get a backlog to catch up on, and there's a standings view you can honourably leave. Rule 01 is finally enforced — the game can be lost, and won. Around that: iCloud sync across devices, dark mode (warm / dark / auto), swipe navigation, and time-of-day greetings that know it's 23:00. The app also got a public face: lettinggo.fireatwill.org.

What I'd do differently

Roadmap

TestFlight — the repo side is done (versioning wired, export compliance answered, release.sh archives and uploads in one command, Xcode Cloud as the no-local-Xcode path), so what's left is console setup, not code. Then a slow, careful App Store submission; the site's "coming soon" pills are waiting to become badges.


Thanks for reading. Found a bug or have a sharper idea? get in touch — or run cat 0x0103 above to see the café app.