Why just flipping to swift 6 is not enough
Let me take you back to the moment I decided to switch my Xcode compiler to Swift 6. I thought this simple flip of a switch would reveal all the necessary errors I needed to fix—boom, done, app updated. Sure enough, I got just two errors, so I patted myself on the back: “Looks like moving to Swift 6 isn’t nearly as difficult as people said!” Well, that blissful optimism didn’t last long.
How I knew I was not fully using Swift 6
I had been working on the iOS version of my app, and as soon as I tested synchronization between iOS and macOS, I noticed something was off: certain transaction data was missing. My categories weren’t showing up. It felt like a data race, where one process swoops in and grabs data while another is still fetching it. But wait—Swift 6 is supposed to prevent data races, right?
I was convinced something deeper was wrong. In a desperate attempt, I forced my data-fetching method in my singleton class to run on the MainActor
. I figured, “Hey, if it’s on the main thread, what could go wrong?” Turns out…plenty.
Behind the scenes
Simply flipping your compiler to Swift 6 isn’t enough. Swift 6 will spin up threads automatically for non-UI code if you don’t explicitly confine that code within an actor. Plus, when you move everything to the MainActor
, the compiler’s concurrency checks basically shrug, because all the code is now lumped into one actor.
So here I was, staring at no errors yet facing data races. It’s like the compiler was saying, “Everything’s fine!” while the app was clearly shouting, “No, it’s really not!” After reading blog posts galore, I realized I should switch from singletons with classes to singletons with actors in Swift 6. That’s when I decided to go all-in.
Pro Tip: Turn on "Strict Concurrency Checking." I discovered this late in the game, but it’s a lifesaver for catching data races.
Moving to actors
The moment I changed my class to an actor, I was bombarded with more than 99 errors! But ironically, that massive error list reassured me—I was finally making a genuine transition to Swift 6. Of course, this meant updating database calls to await
and marking methods as async
, not to mention switching UI calls to tasks. Time-consuming? Absolutely. But doable.
Everything was smooth sailing until I hit: "Class Transaction is not sendable." Considering Transaction
is a core model, this was a big deal. So, I turned to AI.
Why AI is very far from been AGI or replace Software Developers
I asked AI what Sendable
meant and how to handle it. It explained the concept clearly and suggested using @unchecked Sendable
on my model, claiming that since it was only accessed by the data actor, it should be safe. Every AI model I tried said the same thing. Skeptical, I gave in and tried it.
The compiler was happy. The error vanished. I thought, “Hooray, I’m officially on Swift 6!” But in reality, I had just buried my errors. SwiftData models aren’t meant to be Sendable
, and that @unchecked
annotation basically told the compiler to look the other way. Great, now I had even more unaddressed problems under the hood.
Lesson Learned: AI can provide quick answers, but it lacks real-world context. It’s like an ocean of knowledge that’s only a centimeter deep. Beware!
Using Persisten Model ID
Since my model wasn’t Sendable
, I tried passing around its Persistent Model ID, which is Sendable
. The plan was to fetch the actual object using that ID in the model context.
For me, this didn’t go smoothly. I had to enable a special property—includePendingChanges
—in my fetch descriptor to get it to compile without errors. Even after checking various forums, I couldn’t find crystal-clear explanations for why this was necessary.
I used this blog to guide on how to use the ID to fetch and get the object back concurret-programming-in-swiftdata
The final solution
Ultimately, I resorted to DTOs (Data Transfer Objects). Whenever I fetch data, I now create a DTO and pass that around. My model remains tucked away behind the data actor.
Of course, if your model has multiple relationships—like Transaction
-> Categories
-> Subcategories
-> Transactions
—you might need multiple DTOs or find a strategy to avoid descending too far. It’s not the easiest fix, but it works and keeps your data race-free.
Here is the blog that I found that explains better how to implement the DTO and how it has been used with SwiftData Use Swiftdata Like A Boss
Conclusion
Migrating to Swift 6 was a bit of a roller coaster, but now that I’m on the other side, I see the benefits. My code is cleaner, the app is faster, and those synchronization issues that once haunted me are finally gone.
I still have a lot to learn about Swift 6 and concurrency, but I’m already experiencing the payoffs, and I hope my journey can spare you some of the same headaches.
P.S. Stay tuned! I’ll be sharing updates on my app soon. In the meantime, feast your eyes on this new logo…