Migration Challenges from Swift 5.x to Swift 6 in a UIKit

I’m currently working on migrating a sizable UIKit project that heavily uses Combine to Swift 6, and the experience has been somewhat overwhelming due to the strict concurrency changes introduced. The main issue is the extensive use of @MainActor—since UIKit components are inherently MainActor isolated, this causes a cascading effect where protocols and dependencies up the chain need to be marked as @MainActor as well.

Combine complicates things further, as publishers and subscribers often involve @MainActor isolated objects, which adds more complexity in managing concurrency across the app. Handling synchronous return values for @MainActor isolated views becomes tricky, and it seems like there’s no way to avoid creating bridges or workarounds for parts of the app that haven’t migrated to Swift 6 yet.

Has anyone successfully navigated this migration in a clean and efficient way, especially in a project using UIKit and Combine? Are there any examples or resources that illustrate a successful transition? Or is the common approach to just sprinkle @MainActor and nonisolated(unsafe) everywhere?

I'm under the impression that one avoids these problems by sticking with the latest version 5 variant unless you really need strict concurrency. You get a lot of the newest features without the added hassle. And given that you're talking about a UIKit app, one assumes you have a working program. What do you hope to gain with this migration?

In principle, it would be no different from just sticking to Swift5 compilation mode (you have in in Swift 6) and reducing the concurrency checks level in compiler options, except you won't need to sprinkle your code with any new directives.

The point of adopting strict concurrency would be that you are not using Combine any more. You would switch away from Combine to using the async equivalents (GitHub - apple/swift-async-algorithms: Async Algorithms for Swift).

Otherwise, yes, I've been migrating all my apps to strict concurrency and the "cascading" you describe is correct. The use of nonisolated(unsafe) sounds like a Bad Smell (though I do use it for mocks and similar test harnessing). Apple gives you a very good migration guide (Documentation) that tells you what to expect.

As a general statement not sure it is quite true though. I do not recall explicit directions at WWDC to do so either.

2 Likes

This is mandatory for a company wide initiative. Otherwise personally I would stick with 5.x or go full SwiftUI.

It looks horrible in my opinion. If it wasn’t a mandatory work wide initiative, I personally wouldn’t migrate just yet. Unless we also go all in SwiftUI

First, yes, it is quite normal to run into problems here.

I do want to point out that SwiftUI really won't change this situation. Your UI, be it implemented in UIKit or SwiftUI, will need to have MainActor-isolated state. Compared to other projects I've worked on, if you can get away with only adding @MainActor annotations, you are doing pretty good!

It's also completely possible to use Combine within a Swift 6 mode project, but it is challenging. In particular, Combine uses sendable closures that are not annotated, and this can result in real problems. You have to be aware of the implications here and be ready to manually annotate your closures as needed.

The migration guide covers this phenomenon, but focuses on when it occurs via an ObjC library: Documentation