Im new to Swift and Im trying to create an workout WatchApp, I have 2 pages, the initial page that will show the last activity and a button to start workout, and I have another page that will show the current workout.
And I created a WorkoutSession class with NSObject, HKLiveWorkoutBuilderDelegate, HKWorkoutSessionDelegate, ObservableObject
and get the attributes that I need, and when click in start workout, will call a function named startWorkout
to begin collection datas, follow code:
ContentView (Main view):
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)
}
}
Workout View:
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) ?? ""
}
}
And my 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)
}
}
And when I call the workoutSession.startWorkoutSession()
function, I get some errors:
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.
I already tried to change to @State
, @StateObject
but doesn't work,
my expectation was to work with @Published...