I maintain a photography app that manages libraries of tens of thousands of images. One morning I opened it and every photo showed no metadata. No picks, no rejects, no file type classification. Three months of curatorial work — gone from the screen.

The photos themselves were fine. The metadata files on disk were fine. But the database — the internal index my app builds to make everything fast and searchable — had been silently wiped. Two critical columns reset to blank defaults. Every pick status: zero. Every file type: "jpeg," including 52,000 RAW files.

I hadn't changed anything in my app. I'd updated Xcode — Apple's development tool.


A brief primer on SwiftData

When you build an app that needs to remember things — user data, settings, a library of photos — you need a way to store that data persistently. Apple provides a framework called SwiftData for this. You describe your data model in code ("a photo has a rating, a file type, a pick status"), and SwiftData handles the actual storage: creating database tables, saving changes, loading data back on next launch.

The promise is compelling: describe what you want to store, and the framework handles how. You don't write database code. You write Swift.

The problem is what happens when the framework changes its mind about how.


What went wrong

My photo model had two properties stored as Swift enumerations — a type-safe way of saying "this value can only be one of these specific options." Pick status: neutral, picked, or rejected. File type: JPEG, RAW, DNG, TIFF, and so on.

This worked perfectly for months. Then Apple shipped a new version of their Swift compiler, and the machinery that translates Swift code into database tables changed its behavior. The new version looked at those same enum properties and decided to create new database columns for them — with blank default values. The old columns, containing the real data, were left orphaned. Still there in the database, but invisible to the framework.

No warning. No error message. No migration prompt. The app launched, SwiftData read the new empty columns, and every photo appeared to have no pick status and the wrong file type.

This is the software equivalent of a filing clerk who, during a routine office renovation, creates a new set of empty folders with the same labels as the originals — then files everything in the new folders while the originals sit in a box under the desk.


The cost of silent failure

The data was recoverable. That's the good news, and I'll get to why in a moment.

But let's talk about the bad news first: it took an entire day.

Not because the fix was conceptually hard. The fix was straightforward — re-derive the data from other sources. File types can be recomputed from file extensions. Pick status lives in XML metadata files next to each photo on disk. The information existed in multiple places. It just needed to be read back in.

The problem was that every attempt to fix it ran into the framework fighting back:

Attempt one: use the app's built-in rescan feature. This loaded all 73,000 photo records into memory at once, on the main processing thread. The app froze. Force quit.

Attempt two: batch the work into smaller chunks. Still running on the main thread, still freezing the interface. After 45 minutes, only 16,000 of 73,000 photos processed. The app reported as "not responding."

At this point the temptation was strong: delete the database entirely and reimport everything from scratch. A two-to-three-hour process. It would work, but it felt like surrendering to a bug that shouldn't exist.

Attempt three: bypass SwiftData entirely. Drop down to the older Core Data layer underneath, create a separate background processing channel, read the metadata files in batches of 2,000, write directly to the database without touching the user interface. This worked. Three minutes for the full library.

But getting to "three minutes" took hours of wrong turns.

And then came the secondary damage: the recovery had correctly restored pick status, but in doing so it had marked every photo's metadata as "up to date" — which told the app not to re-read the metadata files on the next scan. Copyright information, creator credits, and other fields that lived in those same files were still missing. Another migration. Another hour of debugging to understand why.

Seven commits. One full day. All because a framework update silently changed how data gets stored.


Why the data survived at all

When I started building this app, I made an architectural decision that felt almost philosophical at the time: the database is a cache, not the source of truth.

Every rating, every keyword, every pick/reject flag is written to an XMP sidecar file — a small XML text file that sits right next to the photo on disk. Industry-standard format. Readable by Adobe Lightroom, Capture One, and dozens of other tools. Plain text, human-readable, portable.

The database exists purely for speed. It's an index — like the index at the back of a book. If you tear out the index, the book's content is still there. You just have to rebuild the index.

This isn't a novel idea. It's how I argued photography software should work when I wrote about leaving Lightroom last month. Lightroom's catalog is the data. Lose the catalog, lose your work. In my architecture, losing the database means rebuilding it from the files. Tedious, but not catastrophic.

That architectural decision — made months ago, for philosophical reasons about data ownership — is the only reason this story ends with "I lost a day" instead of "I lost three months of work."


What Apple should do differently

SwiftData is approaching its third birthday. It's past the "it's new, give it time" grace period. For a framework whose entire purpose is storing user data reliably, the following should be non-negotiable:

Silent schema changes are unacceptable. When a compiler update changes how your data model maps to database columns, developers need to know. A warning. An error. A migration prompt. Something. Not silent column creation with default values while real data sits orphaned.

Data loss should be treated as a critical bug. This wasn't an edge case involving exotic Swift features. Enum properties are a basic language construct. The framework compiled them, stored them, and then — on a point release — forgot how to read them. For any other database system, silently dropping columns would be treated as a severity-one incident.

Automatic migration needs to be trustworthy or transparent. The pitch of SwiftData is "declare your model, we handle the rest." That contract is violated when "the rest" includes silently orphaning data. If automatic migration can't handle a case, it should fail loudly — not fail silently and succeed visibly.


The broader lesson

This isn't just a SwiftData story. It's a story about what happens when you build on abstractions you don't control.

Every app developer makes this trade-off. You use frameworks because they save time. You trust them because the vendor is reputable. Apple, Google, Microsoft — these aren't fly-by-night operations. Their frameworks are used by millions of apps.

But frameworks change. Compilers change. The behavior you tested against today may not be the behavior that ships tomorrow. And when the abstraction breaks, it breaks underneath you — in a layer you were specifically told you didn't need to understand.

The only real defense is architectural: keep your source of truth somewhere the framework can't reach. Files on disk. An external backup. A second representation of your data that doesn't depend on the same machinery that failed.

For my app, that's XMP sidecar files. For a note-taking app, it might be Markdown files on disk. For a fitness app, it might be HealthKit as the canonical store with the local database as a derived view. The specific mechanism matters less than the principle: never let a single framework be the only copy of data your users care about.


Where things stand now

The data is recovered. The picks are back. The file types are correct. Copyright and creator information is flowing in from the metadata files.

I've added a startup health check — a few quick database queries that detect the specific pattern of "every value in this column is identical when it shouldn't be." If SwiftData ever silently resets columns again, the app will catch it on the next launch instead of letting me discover it when photos look wrong.

I've also stopped using Swift enumerations as stored database properties entirely. The replacement is less elegant — raw integers and strings with computed accessors — but SwiftData can't misinterpret an integer. It's the kind of defensive coding that a mature framework shouldn't require, but pragmatism beats elegance when your users' data is on the line.

The uncomfortable truth is that I left Adobe's ecosystem partly because I didn't want my data locked inside software I don't control. Then I put my data index inside a framework I don't control. The architecture saved me — the files on disk were always there, untouched, recoverable. But I shouldn't have needed saving. And the hours spent fighting the framework to rebuild its own database could have been hours spent building features, fixing real bugs, or — imagine — taking photographs.


This is part of an ongoing series about building a photography workflow from scratch. Previous: Leaving Lightroom.

The apps mentioned are part of the Photo Suite — a collection of focused tools for the complete photography workflow on macOS.