What’s new in Swift 6.1?

Published on: February 27, 2025

The Xcode 16.3 beta is out, which includes a new version of Swift. Swift 6.1 is a relatively small release that comes with bug fixes, quality of life improvements, and some features. In this post, I’d like to explore two of the new features that come with Swift 6.1. One that you can start using immediately, and one that you can opt-in on if it makes sense for you.

The features I’d like to explore are the following:

  1. Changes to Task Groups in Swift 6.1
  2. Changes to member visibility for imported code

We’ll start by looking at the changes in Concurrency’s TaskGroup and we’ll cover member visibility after.

Swift 6.1 and TaskGroup

There have been a couple of changes to concurrency in Swift 6.1. These were mainly small bug fixes and improvements but one improvement stood out to me and that’s the changes that are made to TaskGroup. If you're not familiar with task groups, go ahead and read up on them on my blog post right here.

Normally, a TaskGroup is created as shown below where we create a task group and specify the type of value that every child task is going to produce:

await withTaskGroup(of: Int.self) { group in
  for _ in 1...10 {
    group.addTask {
      return Int.random(in: 1...10)
    }
  }
}

Starting in Swift 6.1, Apple has made it so that we no longer have to explicitly define the return type for our child tasks. Instead, Swift can infer the return type of child tasks based on the first task that we add to the group.

That means that the compiler will useaddGroup it finds to determine the return type for all your child tasks.

In practice, that means that the code below is the equivalent of what we saw earlier:

await withTaskGroup { group in
  for _ in 1...10 {
    group.addTask {
      return Int.random(in: 1...10)
    }
  }
}

Now, as you might expect, this doesn't change the fact that our task groups have to return the same type for every child task.

The code above shows you how you can use this new return type inference in Swift 6.1. If you accidentally do end up with different return types for your child task like the code below shows, the compiler will present us with an error that will tell you that the return type of your call to addTask is incorrect.

await withTaskGroup { group in
  for _ in 1...10 {
    group.addTask {
      return Int.random(in: 1...10)
    }
  }

  group.addTask {
    // Cannot convert value of type 'String' to closure result type 'Int'
    return "Hello, world"
  }
}

Now, if you find that you do want to have multiple return types, I have a blog post on that. That approach still works. We can still use an enum as a return type for our task group for our child tasks, and that definitely still is a valid way to have multiple return types in a task group.

I’m quite happy with this change because having to specify the return type for my child tasks always felt a little tedious so it’s great to see the compiler take this job in Swift 6.1.

Next, let’s take a look at the changes to imported member visibility in Swift 6.1.

Imported member visibility in Swift 6.1

In Swift, we have the ability to add extensions to types to enhance or augment functionality that we already have. For example, you could add an extension to an Int to represent it as a currency string or something similar.

If I'm building an app where I'm dealing with currencies and purchases and handling money, I might have two packages that are imported by my app. Both packages could be dealing with currencies in some way shape or form and I might have an extension on Int that returns a String which is a currency string as I mentioned earlier.

Here's what that could look like.

// CurrencyKit
func price() -> String {
    let formatter = NumberFormatter()
    formatter.numberStyle = .currency
    formatter.locale = Locale.current

    let amount = Double(self) / 100.0 // Assuming the integer represents cents
    return formatter.string(from: NSNumber(value: amount)) ?? "$\(amount)"
}

// PurchaseParser
func price() -> String {
    let formatter = NumberFormatter()
    formatter.numberStyle = .currency
    formatter.locale = Locale.current

    let amount = Double(self) / 100.0 // Assuming the integer represents cents
    return formatter.string(from: NSNumber(value: amount)) ?? "$\(amount)"
}

The extension shown above exists in both of my packages, and the return types of these extensions are the exact same (i.e., strings). This means that I can have the following two files in my app, and it's going to be just fine.

// FileOne.swift
import PurchaseParser

func dealsWithPurchase() {
    let amount = 1000
    let purchaseString = amount.price()
    print(purchaseString)
}

// FileTwo.swift
import CurrencyKit

func dealsWithCurrency() {
    let amount = 1000
    let currencyString = amount.price()
    print(currencyString)
}

The compiler will know how to figure out which version of price should be used based on the import in my files and things will work just fine.

However, if I have two extensions on integer with the same function name but different return types, the compiler might actually get confused about which version of the extension I intended to use.

Consider the following changes to PurchaseParser's price method:

func price() -> Double {
    let formatter = NumberFormatter()
    formatter.numberStyle = .currency
    formatter.locale = Locale.current

    let amount = Double(self) / 100.0 // Assuming the integer represents cents
    return amount
}

Now, price returns a Double instead of a String. In my app code, I am able to use this extension from any file, even if that file doesn’t explicitly import PurchaseParser. As a result, the compiler isn’t sure what I mean when I write the following code in either of the two files that you saw earlier:

let amount = 1000
let currencyString = amount.price()

Am I expecting currencyString to be a String or am I expecting it to be a Double?

To help the compiler, I can explicitly type currencyString as follows:

let amount = 1000
let currencyString: String = amount.price()

This will tell the compiler which version of price should be used, and my code will work again. However, it’s kind of strange in a way that the compiler is using an extension on Int that’s defined in a module that I didn’t even import in this specific file.

In Swift 6.1, we can opt into a new member visibility mode. This member visibility mode is going to work a little bit more like you might expect.

When I import a specific module like CurrencyKit, I'm only going to be using extensions that were defined on CurrencyKit. This means that in a file that only imports CurrencyKit I won’t be able to use extensions defined in other packages unless I also import those. As a result, the compiler won’t be confused about having multiple extensions with the method name anymore since it can’t see what I don’t import.

Opting into this feature can be done by passing the corresponding feature flag to your package, here's what that looks like when you’re in a Swift package:

.executableTarget(
    name: "AppTarget",
    dependencies: [
        "CurrencyKit",
        "PurchaseParser"
    ],
    swiftSettings: [
        .enableExperimentalFeature("MemberImportVisibility")
    ]
),

In Xcode this can be done by passing the feature to the “Other Swift Flags” setting in your project settings. In this post I explain exactly how to do that.

While I absolutely love this feature, and I think it's a really good change in Swift, it does not solve a problem that I've had frequently. However, I can definitely imagine myself having that problem, so I'm glad that there's now a fix for that that we can opt into. Hopefully, this will eventually become a default in Swift.

In Summary

Overall, Swift 6.1 is a pretty lightweight release, and it has some nice improvements that I think really help the language be better than it was before.

What are your thoughts on these changes in Swift 6.1, and do you think that they will impact your work in any way at all?

Subscribe to my newsletter