스트리밍은 대부분의 브라우저와
Developer 앱에서 사용할 수 있습니다.
-
Swift 6으로 앱을 마이그레이션하기
기존 샘플 앱의 업데이트를 따라 Swift 6 마이그레이션 과정을 직접 경험해 보세요. 증분 마이그레이션 방법을 모듈별로 설명하고, 컴파일러로 데이터 레이스 위험이 있는 코드를 식별하는 방법을 공유합니다. 명확한 분리 경계를 보호하고 공유 가변 상태에 대한 동시 접근을 제거하는 다양한 기술을 배워보세요.
챕터
- 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
리소스
관련 비디오
WWDC21
-
다운로드Array
-
-
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 } }
-
-
찾고 계신 콘텐츠가 있나요? 위에 주제를 입력하고 원하는 내용을 바로 검색해 보세요.
쿼리를 제출하는 중에 오류가 발생했습니다. 인터넷 연결을 확인하고 다시 시도해 주세요.