Swift 6.2 @concurrent in Practice
New Changes to Concurrency
When a list of images is loaded from the filesystem, it’s common to have code that looks something like this:
func makeThumbnail() async -> UIImage? {
return UIImage(contentsOfFile: imagePath)?
.preparingThumbnail(of: CGSize(width: 100, height: 100))
}
var body: some View {
Image(uiImage: thumbnail ?? placeholderImage)
.task {
thumbnail = await makeThumbnail()
}
}
This works great to ensure the work of creating a thumbnail is done on a concurrent background thread.
But what happens when Swift 6.2’s Approachable Concurrency is enabled in Xcode 26?
The Problem: Nonisolated Async Functions Change Behavior
With Approachable Concurrency enabled, nonisolated async functions run on the caller’s actor by default. Since SwiftUI view bodies and .task
closures are bound to @MainActor
, the thumbnail generation suddenly runs on the main thread again - causing a significant hang.
The Fix: @concurrent
to the Rescue
The solution is Swift 6.2’s new @concurrent
attribute, which explicitly switches off from the caller’s actor:
@concurrent func makeThumbnail() async -> UIImage? {
return UIImage(contentsOfFile: imagePath)?
.preparingThumbnail(of: CGSize(width: 100, height: 100))
}
The calling task stays exactly the same.
The result is no more hangs and the images load concurrently again.