我是Swift的新手,我试图创建一个锻炼WatchApp,我有2个页面,初始页面将显示最后一个活动和一个开始锻炼的按钮,我有另一个页面将显示当前的锻炼。
我用NSObject, HKLiveWorkoutBuilderDelegate, HKWorkoutSessionDelegate, ObservableObject
创建了一个WorkoutSession类,并获取我需要的属性,当点击开始锻炼时,将调用一个名为startWorkout
的函数开始收集数据,下面的代码:
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)
}
}
训练视图:
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)
}
}
当我调用workoutSession.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...
1条答案
按热度按时间vql8enpb1#
尝试像这样进行重组:
另外,您最好有一个
View
和@StateObject
对用于授权。然后在该视图的身体检查,如果授权和初始化另一个View
和其他@StateObject
的锻炼。在Apple的Build a workout app for Apple Watch示例中,他们在workoutSession委托方法中使用
DispatchQueue.main.async
来防止[SwiftUI] Publishing changes from background threads is not allowed
。不幸的是,这个例子没有授权失败时的错误处理,所以我仍然建议将授权和训练分离到不同的对象和视图中。