
Migrating to Swift 6 Tutorial
The migration path to Swift 6 is now a lot smoother, with lots more guideposts. Work through this tutorial to find out how much easier it’s become. By Audrey Tam.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Migrating to Swift 6 Tutorial
15 mins
Migrating TheMet to Swift 6.2
Open the project’s Build Settings. Instead of using the incremental approach, which doesn’t raise any issues, enable complete concurrency checking:
Xcode Build Settings: Enable complete checking
Press Command-B to build the project. The build succeeds, but there are warnings:
Warnings raised by complete checking
<code> Sending `self.store` risks causing data races... Capture of `self` with non-sendable type `TheMetStore` in a `@Sendable` closure... </code>
To understand these warnings, you need to know about crossing isolation boundaries and Sendable
types.
Crossing Isolation Boundaries
Isolation domains protect their mutable objects, but your app needs to be able to pass data between domains — some values need to cross isolation boundaries, but this is permitted only where there is no potential for concurrent access to shared mutable state.
The warning arises because of the await
in this line, which occurs twice in ContentView
:
try await store.fetchObjects(for: query)
You’re sending the MainActor
object self.store
to a different isolation domain, and Swift 6 sees this as a data race risk. store
is of type TheMetStore
, and it’s there to call its fetchObjects(for:)
method. The purpose of TheMetStore
is to provide ContentView
with data to display, so it makes sense to include it in the MainActor
isolation domain. Then, you won’t be sending store
across to another isolation domain.
Instead of explicitly marking TheMetStore
as @MainActor
, you’ll set Default Actor Isolation to MainActor … but first, look at the second set of warnings in TheMetStore
.
Sendable Types
Capture of self … in a @Sendable closure
<code> Capture of `self` with non-sendable type `TheMetStore` in a `@Sendable` closure... </code>
A Sendable
type is one that conforms to the Sendable
protocol. Sendable
types are naturally thread safe, and you can send them between different isolation domains with no risk of data races. All value types are Sendable
because their values are copied.
A reference type can be made Sendable
only if it contains no mutable state, and all its immutable properties are also Sendable
. And, the compiler can only validate the implementation of final classes.
Functions can also be @Sendable
. Global function declarations, closures, getters and setters are all @Sendable
by default.
This warning is about the closure in fetchObjects(for:)
:
await MainActor.run {
objects.append(object)
}
Here, self
is TheMetStore
— a class with mutable objects, so it isn’t Sendable
.
You’ll soon set the default actor to MainActor
, and then fetchObjects
will run on the main thread, so objects.append(object)
will run on the main thread. It won’t need to be in an await MainActor.run
closure, so you can delete or comment out those lines:
func fetchObjects(for queryTerm: String) async throws {
print("fetchObjects running on: \(Thread.currentThread)")
if let objectIDs = try await service.getObjectIDs(from: queryTerm) {
print("fetchObjects resuming on: \(Thread.currentThread)")
for (index, objectID) in objectIDs.objectIDs.enumerated()
where index < maxIndex {
if let object = try await service.getObject(from: objectID) {
// await MainActor.run {
objects.append(object)
// }
}
}
}
}
Now, go ahead and change Default Actor Isolation to MainActor in Build Settings:
Xcode Build Settings: Default Actor Isolation — MainActor
Build again, and the warnings go away but now, Circular reference errors appear, as well as warnings about conformance to protocol crossing into main actor-isolated code:
<code> Conformance of 'Object' to protocol 'Encodable' crosses into main actor-isolated code and can cause data races; this is an error in the Swift 6 language mode Main actor-isolated instance method 'encode(to:)' cannot satisfy nonisolated requirement (TheMet.Object.encode) Isolate this conformance to the main actor with '@MainActor' [Apply] Turn data races into runtime errors with '@preconcurrency' [Apply] </code>
Protocol conformance warning
Protocols
From swift.org documentation: A protocol conformance can implicitly affect isolation. However, the protocol’s effect on isolation depends on how the conformance is applied.
The question mark next to the warning links to the swift.org article. Object
and ObjectIDs
are now in the MainActor
isolation domain, but Codable
is non-isolated. The fix is to isolate both protocol conformances to MainActor
.
struct Object: @MainActor Codable, Hashable {
let objectID: Int
let title: String
let creditLine: String
let objectURL: String
let isPublicDomain: Bool
let primaryImageSmall: String
}
struct ObjectIDs: @MainActor Codable {
let total: Int
let objectIDs: [Int]
}
Build and run and watch the console:
Store and Service methods running on main thread
Yes indeed, everything is now running on the main thread, except for the URLSession
method. TheMet is now in phase two of the Swift 6.2 development process. What about phase 3? You need a mock slow process — coming right up!
Adding Concurrency to TheMet
Suppose you want the app to apply some process to each Object
as it arrives, and this process is very slow. Open SlowProcess.swift and add this code to it:
struct SlowProcess {
func doSomething() async throws {
print("doSomething starting on: \(Thread.currentThread)")
try! await Task.sleep(for: Duration(attoseconds: 10000000000))
print("doSomething resuming on: \(Thread.currentThread)")
}
}
The method is a mock slow process — it sleeps for a long time, and the print
statements show which thread it's running on, before and after Task.sleep(for:)
. A real slow process method might do all its work on the same thread so, if it starts on the main thread, all its work is done on the main thread.
In TheMetService
, call doSomething()
in getObject(from:)
, just before return object
:
print("getObject running on: \(Thread.currentThread)")
try! await SlowProcess().doSomething()
print("getObject resuming after slow process on: \(Thread.currentThread)")
Again, you print which thread(s) getObject(from:)
is running on, before and after the call to doSomething()
.
Now, build and run and watch the console.
Slow process running on main thread
SlowProcess
is in the MainActor
isolation domain by default and, sure enough, that's where doSomething
is running! You don't want it slowing down the UI response, so here's how you move it off the main thread.
- Mark
SlowProcess
asnonisolated
. - Mark
doSomething
as@concurrent
.
nonisolated struct SlowProcess { // 1
@concurrent func doSomething() async throws { // 2
print("doSomething starting on: \(Thread.currentThread)")
try! await Task.sleep(for: Duration(attoseconds: 10000000000))
print("doSomething resuming on: \(Thread.currentThread)")
}
}
- Marking
SlowProcess
asnonisolated
uncouples it fromMainActor
, so its methods don't automatically run on the main thread. - Marking
doSomething
as@concurrent
ensures it always runs on a background thread.
Build and run again.
Slow process running on background threads
Great, you've successfully moved doSomething
to background threads, and there's no risk of it slowing down the main thread!