WWDC Quick Look 💓 By SwiftGGTeam
Swift concurrency: Update a sample app

Swift concurrency: Update a sample app

元の動画を見る

ハイライト

Apple は、Coffee Tracker サンプル アプリを使用して、完了ハンドラー、ディスパッチ キュー、コールバック キューを非同期/待機、アクター、MainActor に段階的に移行することを実証し、既存の Swift プロジェクトがより明確な非同期コードとより安全な状態分離を取得できるようにします。

主要内容

Coffee Tracker は watchOS のサンプル アプリです。ユーザーはその日に飲んだコーヒーを記録し、ダイヤルの複雑機構には現在のカフェインレベルが表示されます。

このアプリは小さいですが、実際のプロジェクトに共通する同時実行の問題があります。インターフェイスとモデルはメイン キューで更新されます。バックグラウンド I/O はディスパッチ キューを使用します。 HealthKit の完了ハンドラーは、どのキューからでも返される場合があります。コード サーフェスは UI、モデル、HealthKit の 3 つの層に分割されていますが、ランタイムは複数の実行キューに分散されています。

以前は、この種のコードを保守する場合、開発者は各コールバックがどのキューで実行されたかを覚えておく必要がありました。 HealthKit データを保存するには、完了ハンドラーを作成する必要があります。 HealthKit クエリ結果を読み取ると、コールバックからの処理が続行されます。 SwiftUI モデルを更新する前に、手動でメインスレッドに戻る必要があります。

Apple は、Swift 5.5 で async/await (非同期待機)、アクター (アクター)、および MainActor (メイン アクター) を導入しました。このスピーチで行うことは非常に具体的です。アプリ全体を書き直すのではなく、Coffee Tracker のデータ フローに沿って部分的に移行するだけです。

最初のステップは、既存の非同期 API を次のものに置き換えることです。await電話。 2 番目のステップは、非同期バージョンがまだないコールバック API を継続でラップすることです。 3 番目のステップは、共有状態をアクターに入れることです。最後に UI モデルを次のようにマークします。@MainActor、コンパイラにメインスレッドの分離を任せます。

基礎となる関数から移行を開始する

カフェインのサンプルを保存することから始めるのが良いでしょう。 HealthKit に近く、呼び出しチェーンが短く、変更の範囲が小さいです。

アップルファーストstore.save完了ハンドラーのバージョンは非同期バージョンに置き換えられます。呼び出し元はクロージャを渡すのではなく、直接渡します。try await結果を待ちます。エラー処理は同じ関数内に残ります。

古い API は引き続き存在できます

プロジェクトの移行は通常、一度で完了することはできません。音声は完了ハンドラー エントリを保持し、古いメソッドを非推奨としてマークし、それを内部的に作成します。Task新しい非同期メソッドを呼び出します。

こうすることで、古い呼び出し元は引き続き動作できます。新しいコードは非同期バージョンを直接使用できます。

アクターは同時実行境界を型境界に変換します

HealthKitController は、HealthKit の承認、保存、クエリを担当します。その中にはあるisAuthorizedanchorなどのステータス。以前は、これらの状態には、異なるキューによって同時にアクセスできました。

バンドルHealthKitControllerに変更しますactor最後に、状態アクセスはアクターによって分離されます。そのメソッドへの外部呼び出しには次のものが必要ですawait。同時実行境界は型宣言に書き込まれ、呼び出し元がコードから直接確認できます。

UI モデルを MainActor に渡します

CoffeeData は SwiftUI 用ですObservableObject、 そのcurrentDrinksインターフェースを駆動します。このオブジェクトはメインスレッドで更新する必要があります。

最初にスピーチをするupdateModel(newDrinks:deletedDrinks:)マーク@MainActor、後で全体を入れますCoffeeDataタイプは次のようにマークされています@MainActor。このようにして、モデルの更新と SwiftUI バインディングはメインの実行環境内に残ります。

詳細

1. HealthKit の保存コールバックを async/await に置き換えます。

(07:34) HealthKitHKHealthStore.save(_:)非同期バージョンでは、保存したサンプルをシーケンシャルコードとして記述することができます。成功パスとエラーパスは同じですdo/catch処理中。

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)")
}

キーポイント:

  • doエラーをスローする可能性のある非同期呼び出しをラップします。
  • try await store.save(caffeineSample)HealthKit がサンプルの保存を完了するまで待ちます。
  • self.logger.debug保存が成功した後にのみ実行されます。
  • catch保存失敗エラーをキャッチします。
  • error.localizedDescriptionHealthKit の保存失敗の原因を特定しやすくするためにログを書き込みます。

(09:38) 関数自体も非同期にする必要があります。呼び出し元は関数シグネチャによってハングすることを知っています。

public func save(drink: Drink) async {

キーポイント:

  • public元のアクセスレベルを維持します。
  • func save(drink: Drink)まだ受け取りますDrink
  • async関数が内部的に呼び出せることを示しますawait、呼び出し側も使用する必要がありますawait

(10:15) 同期コンテキストを直接実行することはできません。await、タスクを作成する必要があります。

Task { await self.healthKitController.save(drink: drink) }

キーポイント:

  • Task { ... }新しい非同期タスクを作成します。
  • awaitアクターまたは非同期関数が保存を完了するまで待ちます。
  • self.healthKitController.save(drink: drink)新しく移行した非同期メソッドを再利用します。

2. 古い完了ハンドラーに非同期代替エントリを追加します。

(12:13) 移行プロセス中、古い API が保持されることがあります。 Apple のアプローチは、古いメソッドを非推奨としてマークし、新しい非同期メソッドを内部で呼び出すことです。

@available(*, deprecated, message: "Prefer async alternative instead")
public func requestAuthorization(completionHandler: @escaping (Bool) -> Void ) {
    Task {
        let result = await requestAuthorization()
        completionHandler(result)
    }
}

キーポイント:

  • @available(*, deprecated, message: "Prefer async alternative instead")代わりに非同期バージョンを使用するよう発信者に通知します。
  • completionHandler古い通話フォームをそのままにしておきます。
  • Task同期メソッド内で非同期メソッドを呼び出せるようにします。
  • let result = await requestAuthorization()新しい認可関数を呼び出します。
  • completionHandler(result)非同期結果を古いコールバックにブリッジします。

(14:55) 新しい認証関数は直接戻りますBool。呼び出し元はクロージャ内の結果を読み取る必要がなくなりました。

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
    }
}

キーポイント:

  • async -> Bool関数が非同期で完了することを示し、承認が成功したかどうかを返します。
  • guard isAvailable else { return false }まず、デバイスが HealthKit をサポートしていない状況に対処します。
  • try await store.requestAuthorization(toShare: types, read: types)HealthKit の承認をリクエストします。
  • self.isAuthorized = true認可の成功ステータスを記録します。
  • catch let error認可エラーをキャッチします。
  • ログに記録して失敗時に戻るfalse

3. 継続を使用して、非同期バージョンを使用せずに HealthKit クエリをラップします。

17:43HKAnchoredObjectQueryまだ完了ハンドラーを使用しています。ここで使用されますwithCheckedThrowingContinuationコールバック結果を非同期戻り値に変換します。

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)
    }
}

キーポイント:

  • async throwsクエリがハングするか、エラーがスローされる可能性があることを示します。
  • withCheckedThrowingContinuationエラーをスローする継続を作成します。
  • endDateそしてstartDateサンプルを過去 24 時間のものに制限します。
  • HKQuery.predicateForSamplesHealthKit クエリ条件を構築します。
  • HKAnchoredObjectQuery新しいサンプル、削除されたサンプル、および新しいアンカーをクエリします。
  • if let error = errorクエリの処理に失敗しました。
  • continuation.resume(throwing: error)非同期呼び出し元へのコールバック エラーを処理します。
  • continuation.resume(returning: (samples, deletedSamples, newAnchor))クエリ結果を返します。
  • store.execute(query)HealthKit クエリを開始します。

(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
    }
}

キーポイント:

  • @discardableResult呼び出し元が戻り値を無視できるようにします。
  • guard isAvailableHealthKit をサポートしていないデバイスが続行できないようにします。
  • try await queryHealthKit()新しくラップされたクエリ関数が返されるまで待ちます。
  • self.anchor = newAnchor次の増分クエリのために位置を保存します。
  • drinksToAdd(from:)HealthKit サンプルを次のように変換しますDrink
  • drinksToDelete(from:)削除されたレコードを UUID コレクションに変換します。
  • await MainActor.runモデルの更新をメインの実行環境に切り替えます。
  • catchログクエリの失敗とリターンfalse

4. @MainActor を使用して UI モデルの更新位置を修正します

25:09updateModel(newDrinks:deletedDrinks:)表示に使用するデータが変更されます。マークしてください@MainActor最後に、呼び出し側は手書きする必要がありません。MainActor.run更新ロジック全体をラップします。

@MainActor
private func updateModel(newDrinks: [Drink], deletedDrinks: Set<UUID>) {

キーポイント:

  • @MainActorこのメソッドをメイン アクターに分離します。
  • private表現方法は型内でのみ使用されます。
  • newDrinksHealthKitの新規サンプルから変換したドリンクリストです。
  • deletedDrinksHealthKit で削除されたサンプルに対応する UUID のコレクションです。

(26:43) 発信ポイントで予約されている回線は 1 つだけですawait

await self.updateModel(newDrinks: newDrinks, deletedDrinks: deletedDrinks)

キーポイント:

  • await呼び出しがアクターの境界を越える可能性があることを示します。
  • self.updateModel主役に対して実行されます。
  • コール サイトはメイン キューの切り替えを直接管理しなくなりました。

(50:03) UI モデル全体をメインスレッドで使用する必要がある場合、型全体をマークできます。

@MainActor
class CoffeeData: ObservableObject {

キーポイント:

  • @MainActor全体的に行動するCoffeeDataオンタイプ。
  • CoffeeDataインスタンス メソッドとプロパティへのアクセスは、デフォルトでメイン アクターに設定されます。
  • ObservableObjectSwiftUI にバインドされ、メイン アクターへの配置に適しています。

5. HealthKitController をアクターに変更します

29:24HealthKitControllerHealthKit 関連の状態を管理します。型宣言を次のように変更します。actor最後に、その内部の可変状態はアクターのシリアル化によって保護されます。

actor HealthKitController {

キーポイント:

  • actor分離保護された参照型を定義します。
  • HealthKitController外部同期によって内部状態に自由にアクセスすることはできません。
  • アクター分離メソッドへの外部呼び出しには通常、await

(34:01) 古い完了ハンドラー メソッドはアクター分離状態にアクセスする必要がなく、次のようにマークできます。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 }) {
    // ...
}

キーポイント:

  • nonisolatedこのメソッドがアクター インスタンスに分離されていないことを示します。
  • 2 つの古いメソッドは依然として非推奨としてマークされています。
  • メソッド本体内で作成可能Task、アクターの非同期メソッドを呼び出します。
  • この書き込み方法は段階的な移行に役立ち、古い呼び出し元を保持します。

6. CoffeeDataStore アクターを使用してディスクの読み取りおよび書き込みステータスを分離します

(36:20) このスピーチにより、ディスクの読み書きが以前の状態から変更されました。CoffeeData引き裂いてプライベートアクターに入れます。

private actor CoffeeDataStore {

}

キーポイント:

  • private actor現在のファイルまたはスコープ内でのみ使用されます。
  • CoffeeDataStore特に飲料データのロードと保存を担当します。
  • ディスク I/O 関連の状態が UI モデルから分離されました。

38:37savedValueアクター内に移動して、データが実際に変更されたかどうかを確認します。

private var savedValue: [Drink] = []

キーポイント:

  • private varアクターの内部の可変状態です。
  • [Drink]最後にディスクに書き込まれたドリンク リストを保存します。
  • 初期値[]ドリンクがまだ保存されていないことを示します。

(39:00) ファイルパスも入れますCoffeeDataStore、ストレージの詳細を 1 つのタイプに集中させることができます。

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")
    }
}

キーポイント:

  • private var dataURL: URLplist ファイルのパスを計算します。
  • get throwsパスを取得するとエラーがスローされる可能性があることを示します。
  • FileManager.default.urlユーザー ドキュメント ディレクトリを見つけます。
  • .appendingPathComponent("CoffeeTracker.plist")アプリケーションデータファイル名を追加します。

(45:26) 保存ロジックがアクターに移動された後、savedValueとファイルの書き込みは同じ分離ドメイン内で実行されます。

// 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)")
    }
}

キーポイント:

  • func save(_ currentDrinks: [Drink])現在の UI モデルのドリンクのリストを受け取ります。
  • if currentDrinks == savedValue同じデータを繰り返し書き込むことは避けてください。
  • PropertyListEncoder()plistエンコーダを作成します。
  • encoder.outputFormat = .binaryバイナリ plist を指定します。
  • try encoder.encode(currentDrinks)配列を次のようにエンコードしますData
  • try data.write(to: self.dataURL, options: [.atomic])ファイルにアトミックに書き込みます。
  • self.savedValue = currentDrinks書き込みが成功すると、キャッシュ値が更新されます。
  • catchエンコードの失敗と書き込みの失敗を個別に処理します。

7. async を使用して読み込みデータ ストリームを初期化する

48:01CoffeeDataアクターからディスク データを読み取り、期限切れのドリンクをクリーンアップし、モデルを更新し、HealthKit 認証を要求し、新しいデータをロードするという読み込みプロセスを組み合わせることができるようになりました。

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()
}

キーポイント:

  • func load() async読み込みプロセスがハングすることを示します。
  • await store.load()からCoffeeDataStoreアクターはディスク データを読み取ります。
  • drinks.removeOutdatedDrinks()期限切れの飲み物を削除します。
  • currentDrinks = drinksSwiftUIで使用されるモデルデータを更新します。
  • await self.healthKitController.requestAuthorization()HealthKit の承認をリクエストします。
  • guard success else認証に失敗した場合は早期に復帰します。
  • await self.healthKitController.loadNewDataFromHealthKit()新しい HealthKit データを取得します。

(49:08) イニシャライザを直接非同期としてマークすることはできません。スピーチ用Task読み込みを開始します。

Task { await load() }

キーポイント:

  • Task初期化中に非同期作業を開始します。
  • await load()今すぐロードプロセスを呼び出してください。
  • UI モデルの作成後、タスク内でデータの読み込みが続行されます。

8. バックグラウンド更新で完了ハンドラーを置き換える

(52:18) watchOSのバックグラウンドタスク処理機能も作成可能Task、非同期メソッドを使用して HealthKit の更新を確認します。タスクが完了したら、システム API を呼び出して、バックグラウンド タスクの完了をマークします。

// 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)
}

キーポイント:

  • let model = CoffeeData.shared共有 UI モデルを取得します。
  • Task非同期バックグラウンド更新ロジックを開始します。
  • await model.healthKitController.loadNewDataFromHealthKit()HealthKit から新しいデータを取得します。
  • if success成功した場合にのみ、次のバックグラウンド更新をスケジュールします。
  • scheduleBackgroundRefreshTasks()watchOS のバックグラウンド更新リズムを継続します。
  • backgroundTask.setTaskCompletedWithSnapshot(success)タスクが終了したことをシステムに通知します。

重要ポイント

1. 既存のアプリに対して段階的な非同期/待機移行を実行する

  • 対処方法: システム フレームワークに近い低レベルのサービスから開始し、完了ハンドラー メソッドを非同期メソッドに変更します。
  • 実行する価値がある理由: このスピーチでは、プロジェクト全体を一度に変更することなく、古い方法を維持し、新しい方法で内部の呼び出しチェーンを徐々に置き換えることができることが示されました。
  • 開始方法: 1 つ選択してくださいsaveloadまたはrequestAuthorizationこのような細かい機能が追加されていますasyncバージョン;古い完了ハンドラー メソッドによって内部的に使用されますTask新しいバージョンを呼び出して追加します@available(*, deprecated, message: "Prefer async alternative instead")

2. パッケージにはコールバック API の非同期バージョンがまだありません

  • 何をすべきか: まだ完了ハンドラーに依存しているプロジェクト内のシステム API を非同期関数にラップします。
  • 実行する価値がある理由:HKAnchoredObjectQueryこの例は、継続によってコールバック結果を次のように変えることができることを示しています。try await、上位レベルのビジネス ロジックがよりスムーズになります。
  • 開始方法: プライベート ヘルパーを作成します。queryHealthKit() async throws;存在するwithCheckedThrowingContinuation元のクエリを次の場所に作成します。成功についてresume(returning:)、失敗したときresume(throwing:)

3. キャッシュとファイルの読み取りと書き込みをアクターに組み込む

  • 内容: ローカル キャッシュ、plist/JSON ファイルの読み取りと書き込み、および最後に保存された値用のプライベート アクターを作成します。
  • 実行する価値がある理由:CoffeeDataStoreバンドルsavedValueそしてdataURLUI モデルのバックグラウンド状態を減らすために、すべてが同じアクターに含まれています。
  • 開始方法: 新規private actor DataStore;バンドルsavedValue、ファイル URL、save(_:)load()引っ越してください。外部で使用するawait store.save(value)そしてawait store.load()電話。

4. SwiftUI モデルを @MainActor でマークします。

  • やるべきこと: ドライバーインターフェイスを配置しますObservableObjectまたは次のようにマークされたその更新メソッド@MainActor
  • 実行する価値がある理由: CoffeeData のcurrentDrinksSwiftUI の更新をトリガーします。MainActor の後に配置すると、呼び出し場所が分散されなくなります。DispatchQueue.main.asyncまたはMainActor.run
  • 開始方法: まず変更させてください@Published属性メソッドの追加@MainActor; モデル全体が UI 指向の場合、@MainActorクラス宣言に。

5. バックグラウンドタスクの非同期リンクを書き換えます

  • 何をすべきか: watchOS のバックグラウンド更新、プッシュ処理、スケジュールされた同期、その他の入り口を変更します。Task+ 非同期サービス呼び出し。
  • 実行する価値がある理由: スピーチ内の ExtensionDelegate は、システム コールバック エントリを保持し、内部で非同期メソッドを使用して HealthKit データをロードし、最終的にシステム要件に従ってバックグラウンド タスクを終了します。
  • 開始方法: デリゲートメソッドで作成Task;タスク中awaitモデルまたはサービスを呼び出します。成功後に次のタスクをスケジュールします。最後にシステムの完了 API を呼び出します。

関連セッション

コメント

GitHub Issues · utterances