Streaming is available in most browsers,
and in the Developer app.
-
Migrate your app to Swift 6
Experience Swift 6 migration in action as we update an existing sample app. Learn how to migrate incrementally, module by module, and how the compiler helps you identify code that's at risk of data races. Discover different techniques for ensuring clear isolation boundaries and eliminating concurrent access to shared mutable state.
Chapters
- 0:00 - Introduction
- 0:33 - The Coffee Tracker app
- 0:45 - Review the refactor from WWDC21
- 3:20 - Swift 6 and data-race safety
- 4:40 - Swift 6 migration in practice
- 7:26 - The strategy
- 8:53 - Adopting concurrency features
- 11:05 - Enabling complete checking in the watch extension
- 13:05 - Shared mutable state in global variables
- 17:04 - Shared mutable state in global instances and functions
- 19:29 - Delegate callbacks and concurrency
- 23:40 - Guaranteeing data-race safety with code you don’t maintain
- 25:51 - Enabling the Swift 6 language mode in the watch extension
- 26:35 - Moving on to CoffeeKit
- 27:24 - Enabling complete checking in CoffeeKit
- 27:47 - Common patterns and an incremental strategy
- 29:55 - Global variables in CoffeeKit
- 31:05 - Sending an array between actors
- 33:53 - What if you can’t mark something as Sendable?
- 35:23 - Enabling the Swift 6 language mode in CoffeeKit
- 35:59 - Adding a new feature with guaranteed data-race safety
- 40:43 - Wrap up and the Swift 6 migration guide
Resources
Related Videos
WWDC21
-
DownloadArray
-
-
9:08 - Recaffeinater and CaffeineThresholdDelegate
//Define Recaffeinator class class Recaffeinater: ObservableObject { @Published var recaffeinate: Bool = false var minimumCaffeine: Double = 0.0 } //Add protocol to notify if caffeine level is dangerously low extension Recaffeinater: CaffeineThresholdDelegate { public func caffeineLevel(at level: Double) { if level < minimumCaffeine { // TODO: alert user to drink more coffee! } } }
-
9:26 - Add @MainActor to isolate the Recaffeinator
//Isolate the Recaffeinater class to the main actor @MainActor class Recaffeinater: ObservableObject { @Published var recaffeinate: Bool = false var minimumCaffeine: Double = 0.0 }
-
9:38 - Warning in the protocol implementation
//warning: Main actor-isolated instance method 'caffeineLevel(at:)' cannot be used to satisfy nonisolated protocol requirement public func caffeineLevel(at level: Double) { if level < minimumCaffeine { // TODO: alert user to drink more coffee! } }
-
9:59 - Understanding why the warning is there
//This class is guaranteed on the main actor... @MainActor class Recaffeinater: ObservableObject { @Published var recaffeinate: Bool = false var minimumCaffeine: Double = 0.0 } //...but this protocol is not extension Recaffeinater: CaffeineThresholdDelegate { public func caffeineLevel(at level: Double) { if level < minimumCaffeine { // TODO: alert user to drink more coffee! } } }
-
12:59 - A warning on the logger variable
//var 'logger' is not concurrency-safe because it is non-isolated global shared mutable state; this is an error in the Swift 6 language mode var logger = Logger( subsystem: "com.example.apple-samplecode.Coffee-Tracker.watchkitapp.watchkitextension.ContentView", category: "Root View")
-
13:38 - Option 1: Convert 'logger' to a 'let' constant
//Option 1: Convert 'logger' to a 'let' constant to make 'Sendable' shared state immutable let logger = Logger( subsystem: "com.example.apple-samplecode.Coffee-Tracker.watchkitapp.watchkitextension.ContentView", category: "Root View")
-
14:20 - Option 2: Isolate 'logger' it to the main actor
//Option 2: Annotate 'logger' with '@MainActor' if property should only be accessed from the main actor @MainActor var logger = Logger( subsystem: "com.example.apple-samplecode.Coffee-Tracker.watchkitapp.watchkitextension.ContentView", category: "Root View")
-
14:58 - Option 3: Mark it nonisolated(unsafe)
//Option 3: Disable concurrency-safety checks if accesses are protected by an external synchronization mechanism nonisolated(unsafe) var logger = Logger( subsystem: "com.example.apple-samplecode.Coffee-Tracker.watchkitapp.watchkitextension.ContentView", category: "Root View")
-
15:43 - The right answer
//Option 1: Convert 'logger' to a 'let' constant to make 'Sendable' shared state immutable let logger = Logger( subsystem: "com.example.apple-samplecode.Coffee-Tracker.watchkitapp.watchkitextension.ContentView", category: "Root View")
-
17:03 - scheduleBackgroundRefreshTasks() has two warnings
func scheduleBackgroundRefreshTasks() { scheduleLogger.debug("Scheduling a background task.") // Get the shared extension object. let watchExtension = WKApplication.shared() //warning: Call to main actor-isolated class method 'shared()' in a synchronous nonisolated context // If there is a complication on the watch face, the app should get at least four // updates an hour. So calculate a target date 15 minutes in the future. let targetDate = Date().addingTimeInterval(15.0 * 60.0) // Schedule the background refresh task. watchExtension.scheduleBackgroundRefresh(withPreferredDate: targetDate, userInfo: nil) { //warning: Call to main actor-isolated instance method 'scheduleBackgroundRefresh(withPreferredDate:userInfo:scheduledCompletion:)' in a synchronous nonisolated context error in // Check for errors. if let error { scheduleLogger.error( "An error occurred while scheduling a background refresh task: \(error.localizedDescription)" ) return } scheduleLogger.debug("Task scheduled!") } }
-
17:57 - Annotate function with @MainActor
@MainActor func scheduleBackgroundRefreshTasks() { scheduleLogger.debug("Scheduling a background task.") // Get the shared extension object. let watchExtension = WKApplication.shared() // If there is a complication on the watch face, the app should get at least four // updates an hour. So calculate a target date 15 minutes in the future. let targetDate = Date().addingTimeInterval(15.0 * 60.0) // Schedule the background refresh task. watchExtension.scheduleBackgroundRefresh(withPreferredDate: targetDate, userInfo: nil) { error in // Check for errors. if let error { scheduleLogger.error( "An error occurred while scheduling a background refresh task: \(error.localizedDescription)" ) return } scheduleLogger.debug("Task scheduled!") } }
-
22:15 - Revisiting the Recaffeinater
//This class is guaranteed on the main actor... @MainActor class Recaffeinater: ObservableObject { @Published var recaffeinate: Bool = false var minimumCaffeine: Double = 0.0 } //...but this protocol is not //warning: Main actor-isolated instance method 'caffeineLevel(at:)' cannot be used to satisfy nonisolated protocol requirement extension Recaffeinater: CaffeineThresholdDelegate { public func caffeineLevel(at level: Double) { if level < minimumCaffeine { // TODO: alert user to drink more coffee! } } }
-
22:26 - Option 1: Mark function as nonisolated
//error: Main actor-isolated property 'minimumCaffeine' can not be referenced from a non-isolated context nonisolated public func caffeineLevel(at level: Double) { if level < minimumCaffeine { // TODO: alert user to drink more coffee! } }
-
23:07 - Option 1b: Wrap functionality in a Task
nonisolated public func caffeineLevel(at level: Double) { Task { @MainActor in if level < minimumCaffeine { // TODO: alert user to drink more coffee! } } }
-
23:34 - Option 1c: Explore options to update the protocol
public protocol CaffeineThresholdDelegate: AnyObject { func caffeineLevel(at level: Double) }
-
24:15 - Option 1d: Instead of wrapping it in a Task, use `MainActor.assumeisolated`
nonisolated public func caffeineLevel(at level: Double) { MainActor.assumeIsolated { if level < minimumCaffeine { // TODO: alert user to drink more coffee! } } }
-
25:21 - `@preconcurrency` as a shorthand for assumeIsolated
extension Recaffeinater: @preconcurrency CaffeineThresholdDelegate { public func caffeineLevel(at level: Double) { if level < minimumCaffeine { // TODO: alert user to drink more coffee! } } }
-
26:42 - Add `@MainActor` to the delegate protocol in CoffeeKit
@MainActor public protocol CaffeineThresholdDelegate: AnyObject { func caffeineLevel(at level: Double) }
-
26:50 - A new warning
//warning: @preconcurrency attribute on conformance to 'CaffeineThresholdDelegate' has no effect extension Recaffeinater: @preconcurrency CaffeineThresholdDelegate { public func caffeineLevel(at level: Double) { if level < minimumCaffeine { // TODO: alert user to drink more coffee! } } }
-
27:09 - Remove @preconcurrency
extension Recaffeinater: CaffeineThresholdDelegate { public func caffeineLevel(at level: Double) { if level < minimumCaffeine { // TODO: alert user to drink more coffee! } } }
-
29:56 - Global variables in CoffeeKit are marked as `var`
//warning: Var 'hkLogger' is not concurrency-safe because it is non-isolated global shared mutable state private var hkLogger = Logger( subsystem: "com.example.apple-samplecode.Coffee-Tracker.watchkitapp.watchkitextension.HealthKitController", category: "HealthKit") // The key used to save and load anchor objects from user defaults. //warning: Var 'anchorKey' is not concurrency-safe because it is non-isolated global shared mutable state private var anchorKey = "anchorKey" // The HealthKit store. // warning: Var 'store' is not concurrency-safe because it is non-isolated global shared mutable state private var store = HKHealthStore() // warning: Var 'isAvailable' is not concurrency-safe because it is non-isolated global shared mutable state private var isAvailable = HKHealthStore.isHealthDataAvailable() // Caffeine types, used to read and write caffeine samples. // warning: Var 'caffeineType' is not concurrency-safe because it is non-isolated global shared mutable state private var caffeineType = HKObjectType.quantityType(forIdentifier: .dietaryCaffeine)! // warning: Var 'types' is not concurrency-safe because it is non-isolated global shared mutable state private var types: Set<HKSampleType> = [caffeineType] // Milligram units. // warning: Var 'miligrams' is not concurrency-safe because it is non-isolated global shared mutable state internal var miligrams = HKUnit.gramUnit(with: .milli)
-
30:19 - Change all global variables to `let`
private let hkLogger = Logger( subsystem: "com.example.apple-samplecode.Coffee-Tracker.watchkitapp.watchkitextension.HealthKitController", category: "HealthKit") // The key used to save and load anchor objects from user defaults. private let anchorKey = "anchorKey" // The HealthKit store. private let store = HKHealthStore() private let isAvailable = HKHealthStore.isHealthDataAvailable() // Caffeine types, used to read and write caffeine samples. private let caffeineType = HKObjectType.quantityType(forIdentifier: .dietaryCaffeine)! private let types: Set<HKSampleType> = [caffeineType] // Milligram units. internal let miligrams = HKUnit.gramUnit(with: .milli)
-
30:38 - Warning 1: Sending arrays in `drinksUpdated()`
// warning: Sending 'self.currentDrinks' risks causing data races // Sending main actor-isolated 'self.currentDrinks' to actor-isolated instance method 'save' risks causing data races between actor-isolated and main actor-isolated uses await store.save(currentDrinks)
-
32:04 - Looking at Drink struct
// The record of a single drink. public struct Drink: Hashable, Codable { // The amount of caffeine in the drink. public let mgCaffeine: Double // The date when the drink was consumed. public let date: Date // A globally unique identifier for the drink. public let uuid: UUID public let type: DrinkType? public var latitude, longitude: Double? // The drink initializer. public init(type: DrinkType, onDate date: Date, uuid: UUID = UUID()) { self.mgCaffeine = type.mgCaffeinePerServing self.date = date self.uuid = uuid self.type = type } internal init(from sample: HKQuantitySample) { self.mgCaffeine = sample.quantity.doubleValue(for: miligrams) self.date = sample.startDate self.uuid = sample.uuid self.type = nil } // Calculate the amount of caffeine remaining at the provided time, // based on a 5-hour half life. public func caffeineRemaining(at targetDate: Date) -> Double { // Calculate the number of half-life time periods (5-hour increments) let intervals = targetDate.timeIntervalSince(date) / (60.0 * 60.0 * 5.0) return mgCaffeine * pow(0.5, intervals) } }
-
33:29 - Mark `Drink` struct as Sendable
// The record of a single drink. public struct Drink: Hashable, Codable, Sendable { //... }
-
33:35 - Another type that isn't Sendable
// warning: Stored property 'type' of 'Sendable'-conforming struct 'Drink' has non-sendable type 'DrinkType?' public let type: DrinkType?
-
34:28 - Using nonisolated(unsafe)
nonisolated(unsafe) public let type: DrinkType?
-
34:45 - Undo that change
public let type: DrinkType?
-
35:04 - Change DrinkType to be Sendable
// Define the types of drinks supported by Coffee Tracker. public enum DrinkType: Int, CaseIterable, Identifiable, Codable, Sendable { //... }
-
36:35 - CoreLocation using AsyncSequence
//Create a new drink to add to the array. var drink = Drink(type: type, onDate: date) do { //error: 'CLLocationUpdate' is only available in watchOS 10.0 or newer for try await update in CLLocationUpdate.liveUpdates() { guard let coord = update.location else { logger.info( "Update received but no location, \(update.location)") break } drink.latitude = coord.coordinate.latitude drink.longitude = coord.coordinate.longitude } catch { }
-
38:10 - Create a CoffeeLocationDelegate
class CoffeeLocationDelegate: NSObject, CLLocationManagerDelegate { var location: CLLocation? var manager: CLLocationManager! var latitude: CLLocationDegrees? { location?.coordinate.latitude } var longitude: CLLocationDegrees? { location?.coordinate.longitude } override init () { super.init() manager = CLLocationManager() manager.delegate = self manager.startUpdatingLocation() } func locationManager ( _ manager: CLLocationManager, didUpdateLocations locations: [CLLocation] ) { self.location = locations. last } }
-
39:32 - Put the CoffeeLocationDelegate on the main actor
@MainActor class CoffeeLocationDelegate: NSObject, CLLocationManagerDelegate { var location: CLLocation? var manager: CLLocationManager! var latitude: CLLocationDegrees? { location?.coordinate.latitude } var longitude: CLLocationDegrees? { location?.coordinate.longitude } override init () { super.init() // This CLLocationManager will be initialized on the main thread manager = CLLocationManager() manager.delegate = self manager.startUpdatingLocation() } // error: Main actor-isolated instance method 'locationManager_:didUpdateLocations:)' cannot be used to satisfy nonisolated protocol requirement func locationManager ( _ manager: CLLocationManager, didUpdateLocations locations: [CLLocation] ) { self.location = locations. last } }
-
40:06 - Update the locationManager function
nonisolated func locationManager ( _ manager: CLLocationManager, didUpdateLocations locations: [CLLocation] ) { MainActor.assumeIsolated { self.location = locations. last } }
-
-
Looking for something specific? Enter a topic above and jump straight to the good stuff.
An error occurred when submitting your query. Please check your Internet connection and try again.