Swift Watch健身App中@Published跨类使用遇后台线程发布报错
问题描述
我是Swift新手,正在开发一款Apple Watch健身App,包含两个页面:
- 首页:展示最近一次活动数据,提供「开始训练」按钮
- 训练页面:实时展示当前训练状态
我创建了WorkoutSession类,它遵循NSObject、HKLiveWorkoutBuilderDelegate、HKWorkoutSessionDelegate、ObservableObject协议,通过@Published属性管理训练数据。点击「开始训练」时会调用startWorkoutSession方法启动数据收集,但运行时出现以下错误:
2023-05-09 18:12:14.741560-0300 ApodWatch Watch App[67831:629899] [SwiftUI] Publishing changes from background threads is not allowed; make sure to publish values from the main thread (via operators like receive(on:)) on model updates.
我尝试过替换为@State、@StateObject但都无效,希望能通过正确使用@Published解决问题。
代码实现
ContentView(主视图)
import SwiftUI import HealthKit struct ContentView: View { var body: some View { NavigationView { VStack(alignment: .leading) { VStack(alignment: .leading) { Text("Última atividade") .padding(.bottom, 12) HStack { CaloriesRingView( calories: 168 ) .padding(.trailing, 24) DurationRingView( duration: 5400 ) } }.padding(.top, 12) NavigationLink(destination: WorkoutView()) { Text("Começar treino") } .background(.green) .cornerRadius(20) .padding(.top, 24) } .padding(.horizontal, 12) } .navigationBarBackButtonHidden(true) } }
WorkoutView(训练视图)
import SwiftUI struct WorkoutView: View { @ObservedObject var workoutSession = WorkoutSession() init() { workoutSession.setUpSession() workoutSession.startWorkoutSession() } var body: some View { NavigationView { if workoutSession.status == .inProgress { if workoutSession.bpm != nil { Text(heartBeatFormatter()) } if workoutSession.energyBurned != nil { Text("\(workoutSession.energyBurned!) kcal") } if workoutSession.elapsedTime != nil { Text("\(workoutSession.elapsedTime!)") } } } .navigationBarBackButtonHidden(true) } func heartBeatFormatter() -> String { let formatter = NumberFormatter() formatter.maximumSignificantDigits = 0 return formatter.string(from: workoutSession.bpm! as NSNumber) ?? "" } }
WorkoutSession类
import Foundation import HealthKit import SwiftUI enum WorkoutSessionStatus { case inProgress, complete, cancelled, notStarted, paused } class WorkoutSession: NSObject, HKLiveWorkoutBuilderDelegate, HKWorkoutSessionDelegate, ObservableObject { @Published var energyBurnedStatistics: HKStatistics?; @Published var heartRateStatistics: HKStatistics?; @Published var elapsedTimeStatistics: HKStatistics?; @Published var energyBurned: Double?; @Published var bpm: Double?; @Published var elapsedTime: TimeInterval?; @Published var status = WorkoutSessionStatus.notStarted; @Published var workoutData: HKWorkout? @Published var session: HKWorkoutSession?; @Published var builder: HKLiveWorkoutBuilder?; var healthStore = HKHealthStore(); func workoutBuilder(_ workoutBuilder: HKLiveWorkoutBuilder, didCollectDataOf collectedTypes: Set<HKSampleType>) { self.heartRateStatistics = workoutBuilder.statistics(for: .init(.heartRate)) self.energyBurned = workoutBuilder.statistics(for: .init(.activeEnergyBurned))?.sumQuantity()?.doubleValue(for: .kilocalorie()) self.energyBurnedStatistics = workoutBuilder.statistics(for: .init(.activeEnergyBurned)) self.elapsedTime = workoutBuilder.elapsedTime self.bpm = calculateBPM() } func workoutBuilderDidCollectEvent(_ workoutBuilder: HKLiveWorkoutBuilder) { } func workoutSession(_ workoutSession: HKWorkoutSession, didChangeTo toState: HKWorkoutSessionState, from fromState: HKWorkoutSessionState, date: Date) { } func workoutSession(_ workoutSession: HKWorkoutSession, didFailWithError error: Error) { } func setUpSession() { let typesToShare: Set<HKWorkoutType> = [HKQuantityType.workoutType()] let typesToRead: Set<HKQuantityType> = [ HKQuantityType.quantityType(forIdentifier: .heartRate)!, HKQuantityType.quantityType(forIdentifier: .activeEnergyBurned)!, ] healthStore.requestAuthorization(toShare: typesToShare, read: typesToRead) { (success, error) in if !success { // TODO: Levar para uma página dizendo que só pode usar o app após autorizar } } } func startWorkoutSession() { let configuration = HKWorkoutConfiguration() configuration.activityType = .functionalStrengthTraining configuration.locationType = .indoor do { session = try HKWorkoutSession(healthStore: healthStore, configuration: configuration) builder = session!.associatedWorkoutBuilder() builder!.dataSource = HKLiveWorkoutDataSource(healthStore: healthStore, workoutConfiguration: configuration) session!.delegate = self builder!.delegate = self session!.startActivity(with: Date()) builder!.beginCollection(withStart: Date()) { success, error in if !success { print("Unable to start collection, the error: \(String(describing: error))") return } self.status = .inProgress } } catch { print("Unable to create workout session, the error: \(String(describing: error))") } } func endWorkoutSession() { guard let session = session else { print("Session is nil. Unable to end workout.") return } guard let builder = builder else { print("Builder is nil. Unable to end workout.") return } session.end() builder.endCollection(withEnd: Date()) { success, error in if !success { print("Unable to end collection") return } builder.finishWorkout { workout, error in if workout == nil { print("Unable to read workout") return } self.status = .complete self.workoutData = workout } } } func resumeWorkout() { guard let session = session else { print("Session is nil. Unable to end workout.") return } session.resume() self.status = .inProgress } func pauseWorkout() { guard let session = session else { print("Session is nil. Unable to end workout.") return } session.pause() self.status = .paused } } extension WorkoutSession { private func calculateBPM() -> Double? { let countUnit: HKUnit = .count() let minuteUnit: HKUnit = .minute() let beatsPerMinute: HKUnit = countUnit.unitDivided(by: minuteUnit) return self.heartRateStatistics?.mostRecentQuantity()?.doubleValue(for: beatsPerMinute) } }
解决方案
错误核心原因:HealthKit的回调方法(如workoutBuilder(_:didCollectDataOf:)、授权回调、训练结束闭包)均在后台线程执行,而SwiftUI要求所有@Published属性的更新必须在主线程完成,否则会触发警告。
只需将所有更新@Published属性的代码包裹到DispatchQueue.main.async中,具体修改如下:
1. 修改workoutBuilder(_:didCollectDataOf:)方法
func workoutBuilder(_ workoutBuilder: HKLiveWorkoutBuilder, didCollectDataOf collectedTypes: Set<HKSampleType>) { DispatchQueue.main.async { self.heartRateStatistics = workoutBuilder.statistics(for: .init(.heartRate)) self.energyBurned = workoutBuilder.statistics(for: .init(.activeEnergyBurned))?.sumQuantity()?.doubleValue(for: .kilocalorie()) self.energyBurnedStatistics = workoutBuilder.statistics(for: .init(.activeEnergyBurned)) self.elapsedTime = workoutBuilder.elapsedTime self.bpm = self.calculateBPM() } }
2. 修改startWorkoutSession中的闭包
builder!.beginCollection(withStart: Date()) { success, error in if !success { print("Unable to start collection, the error: \(String(describing: error))") return } DispatchQueue.main.async { self.status = .inProgress } }
3. 修改endWorkoutSession中的闭包
builder.finishWorkout { workout, error in if workout == nil { print("Unable to read workout") return } DispatchQueue.main.async { self.status = .complete self.workoutData = workout } }
4. 优化视图中对象的生命周期管理
在WorkoutView中,应使用@StateObject而非@ObservedObject持有WorkoutSession实例,避免视图重建时重复创建对象:
struct WorkoutView: View { @StateObject var workoutSession = WorkoutSession() // 其余代码不变 }
修改完成后,所有@Published属性的更新都会在主线程执行,警告将消失,数据也能正常同步到SwiftUI视图。
内容的提问来源于stack exchange,提问作者Lucas Buchalla Sesti
相关产品推荐
相关产品推荐

