Swift 5.10: What’s New?
📣 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 “complete” mode ⚠️
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 !
References :
Stackademic 🎓
Thank you for reading until the end. Before you go:
- Please consider clapping and following the writer! 👏
- Follow us X | LinkedIn | YouTube | Discord
- Visit our other platforms: In Plain English | CoFeed | Venture | Cubed
- More content at Stackademic.com