我正在开发一个“HealthController”,他的职责是公开 HealthKit 的助手,并在未来跟踪各种健康包数据点,如心率、卡路里等......
到目前为止,我的设置如下所示(这是在启用完全并发的新 Swift 6 项目中)。
import HealthKit
import SwiftUI
@MainActor @Observable
final class HealthController {
// State
let store = HKHealthStore()
var session: HKWorkoutSession?
var builder: HKLiveWorkoutBuilder?
// Other code for authorization, configuration defaults etc...
func startWatchWorkout(_ name: String, _ startDate: Date) async {
do {
let configuration = HKWorkoutConfiguration()
configuration.activityType = .traditionalStrengthTraining
session = try HKWorkoutSession(healthStore: store, configuration: configuration)
builder = session?.associatedWorkoutBuilder()
builder?.dataSource = HKLiveWorkoutDataSource(healthStore: store, workoutConfiguration: configuration)
session?.startActivity(with: startDate)
try await builder?.beginCollection(at: startDate)
} catch {
// ...
}
}
func endWatchWorkout(_ endDate: Date) async {
do {
session?.end()
try await builder?.endCollection(at: endDate)
session = nil
builder = nil
} catch {
// ...
}
}
}
Controller 被标记为@MainActor,因此可以从 swift ui 视图中调用它的函数。我现在在
builder?.beginCollection(at: startDate)
和 builder?.endCollection(at: endDate)
部分收到以下错误:
发送“HKLiveWorkoutBuilder”类型的主要参与者隔离值 稍后访问非隔离上下文可能会导致数据竞争
不确定这里的正确修复是什么,我尝试将构建器标记为
nonisolated
,但这似乎会导致观察生成错误。我还将每个调用包装在 MainActor.run
内,但它并没有消除错误。
问题在于
builder
和 session
与 MainActor
隔离,但与 Sendable
隔离。您不能对其调用 nonisolated async
方法,因为这最终会将其发送给 off 主要参与者。
您应该在非隔离方法中调用
beginCollection
,因此不存在发送不可发送值的参与者跳跃。您应该只在完成后将构建器和会话分配回实例属性(将它们发送回主要参与者)。如果您在此之前将它们发送回主要参与者,那么您实际上是在主要参与者和之后的非隔离代码之间共享实例,这对于不可发送的值来说当然是不安全的。
// main actor isolated version so that you can easily call from a view
func startWatchWorkout(_ name: String, _ startDate: Date) async {
await startWatchWorkoutImpl(name, startDate)
}
private nonisolated func startWatchWorkoutImpl(_ name: String, _ startDate: Date) async {
do {
let configuration = HKWorkoutConfiguration()
configuration.activityType = .traditionalStrengthTraining
// note that I am not assigning to self.session and self.builder here
let session = try HKWorkoutSession.make(healthStore: store, configuration: configuration)
let builder = session?.associatedWorkoutBuilder()
builder?.dataSource = HKLiveWorkoutDataSource(healthStore: store, workoutConfiguration: configuration)
session?.startActivity(with: startDate)
try await builder?.beginCollection(at: startDate)
await setSessionAndBuilder(session, builder)
} catch {
// ...
}
}
// assigning the properties should be done in a main actor isolated context
private func setSessionAndBuilder(_ s: HKWorkoutSession?, _ b: HKLiveWorkoutBuilder?) {
session = s
builder = b
}