Swift 6.1 may not be a headline-grabbing release, but for those building concurrent systems or UI apps using @MainActor
, it brings subtle yet powerful improvements. In this article, we’ll explore two key changes in Swift’s concurrency system introduced in 6.1 and what they mean for real-world Swift developers.
Swift Concurrency 6.1
Scared of Concurrency???
Running away from Concurrency
Concurrency in Swift has come a long way since the introduction of structured concurrency in Swift 5.5. With every release, the language becomes safer and more ergonomic.
Swift 6.1 continues this journey with improvements that reduce boilerplate and make actor isolation more predictable.
Swift Evolution Proposals help the Swift language grow in a smart, safe, and community-driven way. It’s like a roadmap for the future of Swift, built by both Apple and developers around the world.
SE-0442: Allow TaskGroup’s ChildTaskResult Type To Be Inferred
SE-0449: Allow nonisolated to Prevent Global Actor Inference
Let’s break down each concurrency proposal.
🧵 TaskGroup Inference: Less Code, More Flow
TaskGroup - Reference from CainBlog
Until Swift 6.1, you had to explicitly specify the result type when using withTaskGroup
.
Let me take you back to one of those small frustrations that adds up.
I was inside a TaskGroup block, doing something like:
await withTaskGroup(of: Int.self) { group in
// ...
}
Explicit - Result Type
Every time, I had to specify the result type — even though Swift already knew what I was doing.
Just boilerplate. Repeatedly.
MindBlown stuff
Then came Swift 6.1.
Now, you can just write:
await withTaskGroup { group in
// Result type is inferred 🎉
group.addTask { 10 }
group.addTask { 20 }
...
}
Inferred Result Type
Swift figures out the type from context.
No more .of:, no more type hints everywhere.
Swift can infer the result type automatically
SE-0442 - Before and After Swift 6.1
The currently signature of withTaskGroup(of:returning:body:)
looks like:
public func withTaskGroup<ChildTaskResult, GroupResult>(
of childTaskResultType: ChildTaskResult.Type,
returning returnType: GroupResult.Type = GroupResult.self,
body: (inout TaskGroup<ChildTaskResult>) async -> GroupResult
) async -> GroupResult where ChildTaskResult : Sendable
withTaskGroup Before Swift 6.1
Note that the GroupResult
generic is inferable via the = GroupResult.self
default argument. This can also be applied to ChildTaskResult
as of SE-0326. As in:
public func withTaskGroup<ChildTaskResult, GroupResult>(
of childTaskResultType: ChildTaskResult.Type = ChildTaskResult.self, // <- Updated.
returning returnType: GroupResult.Type = GroupResult.self,
body: (inout TaskGroup<ChildTaskResult>) async -> GroupResult
) async -> GroupResult where ChildTaskResult : Sendable
withTaskGroup After Swift 6.1
Similarly, let’s take another example using withThrowingTaskGroup
Before Swift 6.1
In this example, the of: UserProfile.self parameter explicitly defines the type of results each child task returns.
func fetchUserProfiles(userIDs: [UUID]) async throws -> [UserProfile] {
try await withThrowingTaskGroup(of: UserProfile.self) { group in
for id in userIDs {
group.addTask {
try await fetchUserProfile(for: id)
}
}
var profiles: [UserProfile] = []
// Process Results
}
}
withThrowingTaskGroup - Before Swift 6.1
After Swift 6.1
The compiler can now infer the ChildTaskResult type based on the return type of the addTask closures, allowing you to omit the of parameter:
func fetchUserProfiles(userIDs: [UUID]) async throws -> [UserProfile] {
try await withThrowingTaskGroup { group in
for id in userIDs {
group.addTask {
try await fetchUserProfile(for: id)
}
}
var profiles: [UserProfile] = []
// Process Results
}
}
withThrowingTaskGroup - After Swift 6.1
My code got shorter
The intent became clearer
And it just felt more Swift-like
Allow TaskGroup's ChildTaskResult Type To Be Inferred
SPONSOR
The Unique iOS Swift Conference in the UK
SwiftLeeds is a premier iOS conference taking place on October 7-8 this year. If you’re looking to stay updated on the latest trends in iOS development and connect with like-minded professionals, this event is a must-attend! Don’t miss out—book your tickets now!
Get your tickets!nonisolated
to prevent global actor inferencenonisolated Example
Thinking Hard About Data Isolation
Actor Isolation
If you’ve ever worked on a SwiftUI app, you’ve probably decorated entire types or extensions with @MainActor like this:
@MainActor
protocol ViewBound {}
struct DashboardViewModel: ViewBound {
func fetchData() { ... }
}
Problem
But then you realized…
Oops — now everything inside that extension is @MainActor, including methods that don’t need to be!
You conformed to a protocol that was already @MainActor, and it made your whole type actor-isolated when you didn’t want it to be.
In other words,
When applying @MainActor
to a type or extension, all its members get isolated, sometimes unnecessarily.
@MainActor
protocol GloballyIsolated {}
struct S: GloballyIsolated {} // implicitly globally-isolated
Example From Swift Evolution Proposal
You can now opt out of global actor isolation inference using nonisolated.
Fixed with:
nonisolated struct DashboardViewModel: ViewBound {
func fetchData() { ... } // Runs on background thread, not main
}
Swift 6.1 Changes
You’re telling Swift:
“Yes, I’m conforming to a @MainActor protocol — but don’t isolate this whole type.”
It’s like saying:
“I want the shape of the protocol, not its concurrency contract.”
nonisolated cuts off the automatic actor isolation.
Swift 6.1 lets you apply nonisolated
to an entire type or extension
Another example:
@MainActor
extension ProfileViewModel {
func updateUI() { ... }
}
nonisolated extension ProfileViewModel {
func validateInput() { ... } // Not on main actor
}
Extension - Example
Now, updateUI() runs on the main actor, while validateInput() does not.
SE-0449 - Before and After Swift 6.1
Protocols
nonisolated protocol Refined: GloballyIsolated {}
struct A: Refined {
var x: NonSendable
nonisolated func printX() {
print(x) // okay, 'x' is non-isolated
}
}
nonisolated Protocols
Classes, structs and enums
nonisolated class K: GloballyIsolated {
var x: NonSendable
init(x: NonSendable) {
self.x = x // okay, 'x' is non-isolated
}
}
nonisolated struct S: GloballyIsolated {
var x: NonSendable
init(x: NonSendable) {
self.x = x // okay, 'x' is non-isolated
}
}
nonisolated enum E: GloballyIsolated {
func implicitlyNonisolated() {}
init() {}
}
struct TestEnum {
nonisolated func call() {
E().implicitlyNonisolated() // okay
}
}
nonisolated class, enum, structs
I can now use UI-facing protocols like @MainActor ObservableObject without isolating unrelated logic
Swift lets me choose when isolation is needed — instead of assuming
No repetitive nonisolated
on every method
Cleaner control over which code runs on which thread.
Allow nonisolated to prevent global actor inference
Now I have some confidence in Swift 6.1.
Meme
Swift 6.1 may look small on the surface, but for those writing concurrent Swift, these two changes deliver:
Less boilerplate with inferred task group result types
More control over actor isolation with nonisolated
extensions
Stay tuned for Swift 6.2, which brings even more concurrency enhancements, including isolated deinit
, task naming, and strict memory safety checks.
Have you tried Swift 6.1’s concurrency improvements yet? Share your thoughts or questions in the comments below or on LinkedIn Swift Published
To learn more about the interesting sections in Swift Concurrency
Basics of Swift Concurrency - Strict Concurrency in Swift 6 - Part 1
Swift Proposals of Swift 6 - Swift 6 Unleashed in Xcode 16: Enabling New Features and Strict Concurrency
This is a free third party commenting service we are using for you, which needs you to sign in to post a comment, but the good bit is you can stay anonymous while commenting.