-
Swift concurrency: Update a sample app
Discover Swift concurrency in action: Follow along as we update an existing sample app. Get real-world experience with async/await, actors, and continuations. We'll also explore techniques for migrating existing code to Swift concurrency over time.
To get the most out of this code-along, we recommend first watching “Meet async/await in Swift” and “Protect mutable state with Swift actors” from WWDC21.
Note: To create an async task in Xcode 13 beta 3 and later, use the Task initializer instead.Recursos
Videos relacionados
WWDC22
- Create a more responsive media app
- Eliminate data races using Swift Concurrency
- Visualize and optimize Swift concurrency
WWDC21
- Explore structured concurrency in Swift
- Meet async/await in Swift
- Protect mutable state with Swift actors
- Swift concurrency: Behind the scenes
- What's new in SwiftUI
WWDC20
-
Buscar este video…
-
-
0:07:34 - Call the async version of the HKHealthKitStore save(_:) method
do { try await store.save(caffeineSample) self.logger.debug("\(mgCaffeine) mg Drink saved to HealthKit") } catch { self.logger.error("Unable to save \(caffeineSample) to the HealthKit store: \(error.localizedDescription)") } -
0:09:38 - Change save(drink:) to be an async function
public func save(drink: Drink) async { -
0:10:15 - Create a new asynchronous task
Task { await self.healthKitController.save(drink: drink) } -
0:12:13 - Add an async alternative for requestAuthorization(completionHandler:)
@available(*, deprecated, message: "Prefer async alternative instead") public func requestAuthorization(completionHandler: @escaping (Bool) -> Void ) { Task { let result = await requestAuthorization() completionHandler(result) } } -
0:14:55 - Update the async version of requestAuthorization()
public func requestAuthorization() async -> Bool { guard isAvailable else { return false } do { try await store.requestAuthorization(toShare: types, read: types) self.isAuthorized = true return true } catch let error { self.logger.error("An error occurred while requesting HealthKit Authorization: \(error.localizedDescription)") return false } } -
0:15:43 - Add an async alternative for loadNewDataFromHealthKit(completionHandler:)
@available(*, deprecated, message: "Prefer async alternative instead") public func loadNewDataFromHealthKit(completionHandler: @escaping (Bool) -> Void = { _ in }) { Task { completionHandler(await self.loadNewDataFromHealthKit()) } } -
0:17:43 - Create a queryHealthKit() helper function that uses a continuation
private func queryHealthKit() async throws -> ([HKSample]?, [HKDeletedObject]?, HKQueryAnchor?) { return try await withCheckedThrowingContinuation { continuation in // Create a predicate that only returns samples created within the last 24 hours. let endDate = Date() let startDate = endDate.addingTimeInterval(-24.0 * 60.0 * 60.0) let datePredicate = HKQuery.predicateForSamples(withStart: startDate, end: endDate, options: [.strictStartDate, .strictEndDate]) // Create the query. let query = HKAnchoredObjectQuery( type: caffeineType, predicate: datePredicate, anchor: anchor, limit: HKObjectQueryNoLimit) { (_, samples, deletedSamples, newAnchor, error) in // When the query ends, check for errors. if let error = error { continuation.resume(throwing: error) } else { continuation.resume(returning: (samples, deletedSamples, newAnchor)) } } store.execute(query) } } -
0:20:17 - Update the async version of loadNewDataFromHealthKit()
@discardableResult public func loadNewDataFromHealthKit() async -> Bool { guard isAvailable else { logger.debug("HealthKit is not available on this device.") return false } logger.debug("Loading data from HealthKit") do { let (samples, deletedSamples, newAnchor) = try await queryHealthKit() // Update the anchor. self.anchor = newAnchor // Convert new caffeine samples into Drink instances. let newDrinks: [Drink] if let samples = samples { newDrinks = self.drinksToAdd(from: samples) } else { newDrinks = [] } // Create a set of UUIDs for any samples deleted from HealthKit. let deletedDrinks = self.drinksToDelete(from: deletedSamples ?? []) // Update the data on the main queue. await MainActor.run { // Update the model. self.updateModel(newDrinks: newDrinks, deletedDrinks: deletedDrinks) } return true } catch { self.logger.error("An error occurred while querying for samples: \(error.localizedDescription)") return false } } -
0:25:09 - Annotate updateModel(newDrinks:deletedDrinks:) with @MainActor
@MainActor private func updateModel(newDrinks: [Drink], deletedDrinks: Set<UUID>) { -
0:26:43 - Remove MainActor.run from the call site of updateModel(newDrinks:deletedDrinks:)
await self.updateModel(newDrinks: newDrinks, deletedDrinks: deletedDrinks) -
0:29:24 - Change HealthKitController to be an actor
actor HealthKitController { -
0:32:31 - Move updateModel(newDrinks:deletedDrinks:) to CoffeeData
@MainActor public func updateModel(newDrinks: [Drink], deletedDrinks: Set<UUID>) { guard !newDrinks.isEmpty && !deletedDrinks.isEmpty else { logger.debug("No drinks to add or delete from HealthKit.") return } // Remove the deleted drinks. var drinks = currentDrinks.filter { deletedDrinks.contains($0.uuid) } // Add the new drinks. drinks += newDrinks // Sort the array by date. drinks.sort { $0.date < $1.date } currentDrinks = drinks } -
0:33:18 - Update the call site of updateModel(newDrinks:deletedDrinks:)
await model?.updateModel(newDrinks: newDrinks, deletedDrinks: deletedDrinks) -
0:34:01 - Mark the deprecated completion handler methods as nonisolated
@available(*, deprecated, message: "Prefer async alternative instead") nonisolated public func requestAuthorization(completionHandler: @escaping (Bool) -> Void ) { // ... } @available(*, deprecated, message: "Prefer async alternative instead") nonisolated public func loadNewDataFromHealthKit(completionHandler: @escaping (Bool) -> Void = { _ in }) { // ... } -
0:36:20 - Create a private CoffeeDataStore actor for loading and saving
private actor CoffeeDataStore { } -
0:36:43 - Add a dedicated logger for CoffeeDataStore
let logger = Logger(subsystem: "com.example.apple-samplecode.Coffee-Tracker.watchkitapp.watchkitextension.CoffeeDataStore", category: "ModelIO") -
0:37:05 - Add an instance of the actor to CoffeeData
private let store = CoffeeDataStore() -
0:38:37 - Move the savedValue property from CoffeeData to CoffeeDataStore
private var savedValue: [Drink] = [] -
0:39:00 - Move the dataURL property from CoffeeData to CoffeeDataStore
private var dataURL: URL { get throws { try FileManager .default .url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false) // Append the file name to the directory. .appendingPathComponent("CoffeeTracker.plist") } } -
0:42:42 - Move the didSet for currentDrinks to a new async function
@Published public private(set) var currentDrinks: [Drink] = [] private func drinksUpdated() async { logger.debug("A value has been assigned to the current drinks property.") // Update any complications on active watch faces. let server = CLKComplicationServer.sharedInstance() for complication in server.activeComplications ?? [] { server.reloadTimeline(for: complication) } // Begin saving the data. await store.save(currentDrinks) } -
0:44:00 - Update addDrink(mgCaffeine:onData:) to call drinksUpdated()
// Save drink information to HealthKit. Task { await self.healthKitController.save(drink: drink) await self.drinksUpdated() } -
0:44:09 - Update updateModel(newDrinks:deletedDrinks:) to call drinksUpdated()
await drinksUpdated() -
0:44:17 - Mark the updateModel(newDrinks:deletedDrinks:) method as async
@MainActor public func updateModel(newDrinks: [Drink], deletedDrinks: Set<UUID>) async { -
0:45:26 - Complete the move of the save() method into CoffeeDataStore
// Begin saving the drink data to disk. func save(_ currentDrinks: [Drink]) { // Don't save the data if there haven't been any changes. if currentDrinks == savedValue { logger.debug("The drink list hasn't changed. No need to save.") return } // Save as a binary plist file. let encoder = PropertyListEncoder() encoder.outputFormat = .binary let data: Data do { // Encode the currentDrinks array. data = try encoder.encode(currentDrinks) } catch { logger.error("An error occurred while encoding the data: \(error.localizedDescription)") return } // Save the data to disk as a binary plist file. do { // Write the data to disk. try data.write(to: self.dataURL, options: [.atomic]) // Update the saved value. self.savedValue = currentDrinks self.logger.debug("Saved!") } catch { self.logger.error("An error occurred while saving the data: \(error.localizedDescription)") } } -
0:46:20 - Move the top part of the load() method into CoffeeDataStore
func load() -> [Drink] { logger.debug("Loading the model.") var drinks: [Drink] do { // Load the drink data from a binary plist file. let data = try Data(contentsOf: self.dataURL) // Decode the data. let decoder = PropertyListDecoder() drinks = try decoder.decode([Drink].self, from: data) logger.debug("Data loaded from disk") } catch CocoaError.fileReadNoSuchFile { logger.debug("No file found--creating an empty drink list.") drinks = [] } catch { fatalError("*** An unexpected error occurred while loading the drink list: \(error.localizedDescription) ***") } // Update the saved value. savedValue = drinks return drinks } -
0:48:01 - Update the load() method in CoffeeData to use the actor
func load() async { var drinks = await store.load() // Drop old drinks drinks.removeOutdatedDrinks() // Assign loaded drinks to model currentDrinks = drinks // Load new data from HealthKit. let success = await self.healthKitController.requestAuthorization() guard success else { logger.debug("Unable to authorize HealthKit.") return } await self.healthKitController.loadNewDataFromHealthKit() } -
0:49:08 - Update the CoffeeData initializer to use an async task
Task { await load() } -
0:50:03 - Annotate CoffeeData with @MainActor
@MainActor class CoffeeData: ObservableObject { -
0:52:18 - Replace the completion handler usage in the handle(_:) method of ExtensionDelegate
// Check for updates from HealthKit. let model = CoffeeData.shared Task { let success = await model.healthKitController.loadNewDataFromHealthKit() if success { // Schedule the next background update. scheduleBackgroundRefreshTasks() self.logger.debug("Background Task Completed Successfully!") } // Mark the task as ended, and request an updated snapshot, if necessary. backgroundTask.setTaskCompletedWithSnapshot(success) }
-