Home Articles

Swift Concurrency 6.1: Smarter Isolation with nonisolated & Global Actor Control

What’s New in Swift 6.1 for Concurrency 🧵⚡

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

Swift Concurrency 6.1


Scared of Concurrency???

Running away from Concurrency

Running away from Concurrency

Why Swift Concurrency Matters

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

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.

What’s new in Swift 6.1

  • SE-0442: Allow TaskGroup’s ChildTaskResult Type To Be Inferred

  • SE-0449: Allow nonisolated to Prevent Global Actor Inference

Deep-Dive Into Each Proposal

Let’s break down each concurrency proposal.

SE-0442: Allow TaskGroup's ChildTaskResult Type To Be Inferred


🧵 TaskGroup Inference: Less Code, More Flow

TaskGroup - Reference from CainBlog

TaskGroup - Reference from CainBlog

Problem

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

MindBlown stuff

What’s changed?

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

Code Comparison

SE-0442 - Before and After Swift 6.1

SE-0442 - Before and After Swift 6.1

Detailed Information

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

Why it matters

  • My code got shorter

  • The intent became clearer

  • And it just felt more Swift-like

🔗 Swift Evolution Link

Allow TaskGroup's ChildTaskResult Type To Be Inferred


SPONSOR

iOS Conference AI Image

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!

SE-0449: Allow nonisolated to prevent global actor inference

nonisolated Example

nonisolated Example


Thinking Hard About Data Isolation

Actor Isolation

Actor Isolation

Problem

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

What’s changed?

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.

Code Comparison

SE-0449 - Before and After Swift 6.1

SE-0449 - Before and After Swift 6.1

Detailed Information

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

Why it matters

  • 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.

🔗 Swift Evolution Link

Allow nonisolated to prevent global actor inference

Now I have some confidence in Swift 6.1.

Meme

Meme

Summary

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

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.