We have been trying to do an incremental migration to Swift 6, but found that this is seems to be unexpectedly risky. In particular, the way that Swift 6 will crash if a @MainActor function is called off the main actor, combined with the way Swift 5 mode opens up huge loopholes through the @MainActor enforcement and the way Apple APIs are still quite concurrency unaware seems to in practice make it very unwise to migrate parts of your app to Swift 6 while leaving others in Swift 5 mode.
So we are looking at just enabling all upcoming features but leaving everything in Swift 5 mode until the migration is complete, and then re-evaluate if it is safe to turn on Swift 6 mode. The question, however, is:
Is this actually viable? Will code compiled in Swift 5 mode with all Swift 6 features enabled actually not crash on wrongly called @MainActor functions?
What are the actual differences between Swift 5 with all upcoming features enabled, and Swift 6? Are there any other differences to be aware of?
So far, we've noticed that the Swift 5 compiler is less good at inferring certain things, so going back from Swift 6 -> Swift 5 + all upcoming does introduce some new warnings that need to be manually fixed. Also, in Swift 6 @MainActor is inherently Sendable, while it is not in Swift 5, so it requires adding some more Sendable.
I just waned to note here that the Swift 5 compiler has many less features than the 6 compiler, and is absolutely more difficult to use for concurrency.
However, all of the Swift 6 mode features of the Swift 6 compiler are possible to enable with flags in Swift 5 mode. For example, the sendability differences of globally-isolated closures is controllable via the GlobalActorIsolatedTypesUsability and is super useful if you are using concurrency with Swift 5 mode.