My experience (attempting) a migration to swift 6

I recently migrated several iOS apps to use Swift 6 with concurrency checking and this is what I found. Well, tried to anyways.
These are iOS apps that fetch and display json with a couple of third party libraries and few data oriented classes, and minimal threading. Most data manipulation is through structs and codable.

First I changed language version to 6 in Xcode and concurrency checking to minimal. Cleaned folders etc. then started the first build.
I was shown 50 or so errors at first and I thought ok not bad. I sprinkled mainactor on all the classes to make the red errors go away. Then came 50 more errors… half a week later of repeating this process they finally built.

I had to comment out a bunch of functionality cause I couldn’t find a way to make it easily work but I didn’t spend much time on any single error or warning.

Afterwards I looked back and found I could remove some of the mainactor annotations that I had carelessly added. I feel like since there were sooo many errors that I didn’t know what were the real issues or if Xcode gave up. So I think main actor by default is badly needed for app developers.

Third party libraries were fairly easy to deal with - just add @preconcurrency to the import or protocol and move on.

Some obj c code - I have no clue so it got commented out for the time being.

A few issues with legacy/spaghetti code cropped up but could be addressed with nonisolated(unsafe). We need to dynamically load data based on users’ status and the existing code would require refactoring to fix correctly.

Deinits seem useless or unusable now? I guess I need to refactor this functionally to viewDidDisappear?

Now that I have experience with green and brown field projects with greenfield being easy, it feels like Swift 6 pushes you to make better architecture decisions.

But brown field I was overwhelmed with errors that weren’t really errors but that’s where compilation failed or the build system halted. Often times Xcode would give no proper errors that I could act on but after clicking around in some files it would find something I could address. I can’t imagine how it’s possible to migrate larger projects.

I think what would help is some sort of softened “strict” system to help migrate. In this hypothetical system it would follow through with the build even after finding concurrency build errors and then surface them to the user. Right now it’s whack a mole and very challenging cause you have to “fix” everything first. Some of the fixes are incorrect in the moment but you need to do it to get it to compile. Or even a “per file” mode to minimize the surface area.

Cause in order to fix any real errors you need to get the code to compile.. but you can’t compile cause there are a ton of errors everywhere. It’s hard to tell what is important and is a real concurrency error or where Xcode/swiftc gave up.

On my newer apps it works great and I think it helps enforce well known design patterns but on existing projects it’s hard to find real errors. Building in swift 5 mode gives some “this is an error in swift 6” but they were no where near enough to even get minimal checking to pass.

Overall I don’t think it was too bad but getting to the point where I could see actual concurrency errors took the most work. Apps that have utilized spm will have a much easier time!

So for beginners my advice is - depending on the size of your app just sprinkle mainactor everywhere then go back and reassess once it compiles :smiley:.

I’m curious if this is the experience others have had.

This is good feedback!

We should do something to warn about this configuration. It is a footgun because, to a first approximation, Swift language version 6 is strict concurrency checking, so you’ve opted into the highest level of checking right off the bat.

We have a migration guide which details the suggested path to updating an existing project. Essentially, it details flags for incrementally ramping up concurrency checking in the Swift 5 language mode. Would be great to hear how that works for your next migration :slight_smile:

1 Like

Oh wow. I even read that guide multiple times! It’s easy to miss or forget a detail. So start in 5 mode!, enable a flag, assess, repeat.

1 Like

Yes. Eventually you'll want to be in Swift 5 mode with complete checking enabled, and most of the upcoming feature flags enabled in Xcode, especially things like the key path and closure sensibility inference. Once you fix all of those warnings, you might be ready for Swift 6 mode. However, Swift 6 mode enables runtime concurrency assertions which will crash any code that isn't completely correct from the concurrency runtime's perspective. This includes Apple's frameworks like Combine, so there may be a whole other process to track down and fix those issues.

Specifically, you'll want to turn on (in Xcode 16.3):

  • "Disable Outward Actor Isolation Inference"
  • "Global-Actor-Isolated Types Usability"
  • "Infer Sendable for Methods and Key Path Literals"
  • "Isolated Default Values"
  • "Isolated Global Variables"
  • "Region Based Isolation"

Most of these can help as you move from minimal to targeted to complete concurrency checking, since they often make the compiler smarter about when to require concurrency markup.

1 Like