Swift 5.10: What’s New?

Jihène Mejri
Bforbank Tech
Published in
5 min readMar 18, 2024

📣 Context :

We have known Swift for 10 years, and with each version we have generally had an iterative improvement of a few features.
But it’s a little different with this version 5.10, which is mainly focused on improving concurrency.

As a reminder, the first concurrency tools arrived in version 5.5 of 2021 (we are mainly talking about Async Await, Actor and Sendable Protocol)

Reminder : What is Sendable protocol ?

One of the Swift 5.5 features that arrived with Concurrency in 2021 was the Sendable protocol.

🔐 This protocol secures access to non-thread-safe class instances.
All “Actors” implicitly conform to Sendable, because Actors ensure that all accesses to their mutable state are done sequentially.

/// While both local and distributed actors are conceptually "actors", there are
/// some important isolation model differences between the two, which make it
/// impossible for one to refine the other.
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
public protocol AnyActor : AnyObject, Sendable {
}

Basic data types such as Integers, Strings, Tuples, Structs, and Enums implicitly adhere to the Sendable due to being value types, making them secure ✅ for transmission between concurrency domains as long as each member conforms to the Sendable protocol :

extension Int : Sendable {
...
}
//Implicitly conforms to Sendable
struct Article {
var views: Int
}

On the other hand, the Article class and any reference type such as closures are not implicitly compliant :

//Does not implicitly conform to Sendable
class Article {
var views: Int
}

Sendable classes must be :

(1) Marked final

(2) Contain only stored properties that are immutable and sendable

(3) Have no superclass or have NSObject as the superclass.

//Conforms to Sendable
final class Article: Sendable {
let views = 22
}

To go further : https://developer.apple.com/videos/play/wwdc2021/10133/

Swift 5.10 : what’s new ?

Let’s go back to our main topic !

In Swift’s Github changelogs, Apple developers say that version 5.10 checks all potential risks for multiple access to data when xcode is in “completemode ⚠️

Example 1 :

@MainActor
class MyModel {
init() {
MainActor.assertIsolated()
}
static let shared = MyModel()
}
func usedShared() async {
let model = Mymodel.shared
}

await useShared()

What is the problem here ? 🤔

Here even if we configure our project in complete mode with Swift 5.9 we will have a crash during runtime and we cannot see the problem during compilation.

However, with version 5.10 we will have the following warning:

warning: expression is 'async' but is not marked with 
'await' let model = MyModel.shared ^~~~~~~~~~~~~~ await

let model = await Mymodel.shared

Example 2 : out-of-actor detection

@MainActor func requiresMainActor() -> Int {
MainActor.assertIsolted()
return 0
}
@MainActor struct SomeStruct {
var x = requiresMainActor()
var y: Int
}

nonisolated func call() async {
let s = await SomeStruct(y: 10)
print(await s.x)
}

Here we have a @MainActor attribute, so all the manipulations with its instances must be done on the main thread.

Also, one of its properties (var x) has a default value, so when you call the initializer you can omit that property => SomeStruct(y: 10)

⛔ And here where problems start, because the default values don’t appear in this call :

@MainActor func requiresMainActor() -> Int {
MainActor.assertIsolted()
return 0
}
@MainActor struct SomeStruct {
var x = requiresMainActor()
var y: Int
}

nonisolated func call() async {
let s = await SomeStruct(x: requiresMainActor(), y: 10)
print(await s.x)
}

Here the operation in not occurring in the Main Actor and with Swift 5.9 we can’t evaluate this problem in the compilation time.

In Swift 5.10 with a complete concurrency checking we can garantee that the default parameter will be evaluated in the right context.

✅ let s = await SomeStruct(x: await requiresMainActor(), y: 10)

SE-0412 : Strict concurrency for global variables

Global and static variables can be accessed from anywhere in your code, so they are required to either be :

1. Isolated to a global actor, or

2. Immutable and of Sendable type.

Example :

var mutableGlobal = 1
// warning: var 'mutableGlobal' is not concurrency-safe because it is non-isolated global shared mutable state
// (unless it is top-level code which implicitly isolates to @MainActor)

@MainActor func mutateGlobalFromMain() {
mutableGlobal += 1
}

nonisolated func mutateGlobalFromNonisolated() async {
mutableGlobal += 10
}

struct S {
static let immutableSendable = 10
// okay; 'immutableSendable' is safe to access concurrently because it's immutable and 'Int' is 'Sendable'
}

In Swift 5.10 with a complete concurrency checking we can detect this problem in compilation time :

Example 2 : Sendable opt-out

When building the above code with -strict-concurrency=complete, theAll uses of globalCache are guarded by cacheQueue.async { … }, so this code is free of data races in practice.

In this case, nonisolated(unsafe) can be applied to the static variable to silence the concurrency warning :

⚠️ We can use @unchecked Sendable to opt out all the class !

--

--