Dunfey · Hotel WWDC as data, est. 1983
Front desk everything
Years
Topics

2021 Swift

WWDC21 · 1h 1m · Swift

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.

Watch at developer.apple.com ↗

Transcript all transcripts

Code shown on screen · 29 snippets

Call the async version of the HKHealthKitStore save(_:) method swift · at 7:34 ↗
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)")
}
Change save(drink:) to be an async function swift · at 9:38 ↗
public func save(drink: Drink) async {
Create a new asynchronous task swift · at 10:15 ↗
Task { await self.healthKitController.save(drink: drink) }
Add an async alternative for requestAuthorization(completionHandler:) swift · at 12:13 ↗
@available(*, deprecated, message: "Prefer async alternative instead")
public func requestAuthorization(completionHandler: @escaping (Bool) -> Void ) {
    Task {
        let result = await requestAuthorization()
        completionHandler(result)
    }
}
Update the async version of requestAuthorization() swift · at 14:55 ↗
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
    }
}
Add an async alternative for loadNewDataFromHealthKit(completionHandler:) swift · at 15:43 ↗
@available(*, deprecated, message: "Prefer async alternative instead")
public func loadNewDataFromHealthKit(completionHandler: @escaping (Bool) -> Void = { _ in }) {
    Task { completionHandler(await self.loadNewDataFromHealthKit()) }
}
Create a queryHealthKit() helper function that uses a continuation swift · at 17:43 ↗
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)
    }
}
Update the async version of loadNewDataFromHealthKit() swift · at 20:17 ↗
@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
    }
}
Annotate updateModel(newDrinks:deletedDrinks:) with @MainActor swift · at 25:09 ↗
@MainActor
private func updateModel(newDrinks: [Drink], deletedDrinks: Set<UUID>) {
Remove MainActor.run from the call site of updateModel(newDrinks:deletedDrinks:) swift · at 26:43 ↗
await self.updateModel(newDrinks: newDrinks, deletedDrinks: deletedDrinks)
Change HealthKitController to be an actor swift · at 29:24 ↗
actor HealthKitController {
Move updateModel(newDrinks:deletedDrinks:) to CoffeeData swift · at 32:31 ↗
@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
}
Update the call site of updateModel(newDrinks:deletedDrinks:) swift · at 33:18 ↗
await model?.updateModel(newDrinks: newDrinks, deletedDrinks: deletedDrinks)
Mark the deprecated completion handler methods as nonisolated swift · at 34:01 ↗
@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 }) {
    // ...
}
Create a private CoffeeDataStore actor for loading and saving swift · at 36:20 ↗
private actor CoffeeDataStore {

}
Add a dedicated logger for CoffeeDataStore swift · at 36:43 ↗
let logger = Logger(subsystem: "com.example.apple-samplecode.Coffee-Tracker.watchkitapp.watchkitextension.CoffeeDataStore", category: "ModelIO")
Add an instance of the actor to CoffeeData swift · at 37:05 ↗
private let store = CoffeeDataStore()
Move the savedValue property from CoffeeData to CoffeeDataStore swift · at 38:37 ↗
private var savedValue: [Drink] = []
Move the dataURL property from CoffeeData to CoffeeDataStore swift · at 39:00 ↗
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")
    }
}
Move the didSet for currentDrinks to a new async function swift · at 42:42 ↗
@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)
}
Update addDrink(mgCaffeine:onData:) to call drinksUpdated() swift · at 44:00 ↗
// Save drink information to HealthKit.
Task {
    await self.healthKitController.save(drink: drink)
    await self.drinksUpdated()
}
Update updateModel(newDrinks:deletedDrinks:) to call drinksUpdated() swift · at 44:09 ↗
await drinksUpdated()
Mark the updateModel(newDrinks:deletedDrinks:) method as async swift · at 44:17 ↗
@MainActor
public func updateModel(newDrinks: [Drink], deletedDrinks: Set<UUID>) async {
Complete the move of the save() method into CoffeeDataStore swift · at 45:26 ↗
// 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)")
    }
}
Move the top part of the load() method into CoffeeDataStore swift · at 46:20 ↗
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
}
Update the load() method in CoffeeData to use the actor swift · at 48:01 ↗
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()
}
Update the CoffeeData initializer to use an async task swift · at 49:08 ↗
Task { await load() }
Annotate CoffeeData with @MainActor swift · at 50:03 ↗
@MainActor
class CoffeeData: ObservableObject {
Replace the completion handler usage in the handle(_:) method of ExtensionDelegate swift · at 52:18 ↗
// 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)
}

Resources