11import SwiftUI
2+ import HealthKit
3+ import WatchKit
4+ import Combine
5+
6+ class WorkoutManager : NSObject , ObservableObject , HKWorkoutSessionDelegate {
7+ @Published var isRunning = false
8+ let healthStore = HKHealthStore ( )
9+ var session : HKWorkoutSession ?
10+
11+ func requestAuthorization( ) {
12+ let typesToShare : Set = [ HKQuantityType . workoutType ( ) ]
13+ let typesToRead : Set = [
14+ HKQuantityType . quantityType ( forIdentifier: . heartRate) !,
15+ HKQuantityType . quantityType ( forIdentifier: . activeEnergyBurned) !
16+ ]
17+
18+ healthStore. requestAuthorization ( toShare: typesToShare, read: typesToRead) { _, _ in }
19+ }
20+
21+ func toggleWorkout( ) {
22+ if isRunning {
23+ session? . end ( )
24+ isRunning = false
25+ } else {
26+ startWorkout ( )
27+ }
28+ }
29+
30+ func startWorkout( ) {
31+ let configuration = HKWorkoutConfiguration ( )
32+ configuration. activityType = . traditionalStrengthTraining
33+ configuration. locationType = . indoor
34+
35+ do {
36+ session = try HKWorkoutSession ( healthStore: healthStore, configuration: configuration)
37+ session? . delegate = self
38+ session? . startActivity ( with: Date ( ) )
39+ DispatchQueue . main. async {
40+ self . isRunning = true
41+ }
42+ } catch {
43+ print ( " Failed to start workout session: \( error. localizedDescription) " )
44+ }
45+ }
46+
47+ func workoutSession( _ workoutSession: HKWorkoutSession , didChangeTo toState: HKWorkoutSessionState , from fromState: HKWorkoutSessionState , date: Date ) {
48+ DispatchQueue . main. async {
49+ self . isRunning = ( toState == . running || toState == . paused)
50+ }
51+ }
52+
53+ func workoutSession( _ workoutSession: HKWorkoutSession , didFailWithError error: Error ) {
54+ DispatchQueue . main. async {
55+ self . isRunning = false
56+ }
57+ }
58+ }
259
360struct ContentView : View {
61+ @StateObject private var workoutManager = WorkoutManager ( )
62+ @State private var selectedTab = 1
63+
64+ var body : some View {
65+ TabView ( selection: $selectedTab) {
66+ VStack {
67+ Button ( action: {
68+ workoutManager. toggleWorkout ( )
69+ } ) {
70+ Text ( workoutManager. isRunning ? " Stop Session " : " Start Session " )
71+ . font ( . headline)
72+ . padding ( )
73+ }
74+ . tint ( workoutManager. isRunning ? . red : . green)
75+ }
76+ . tag ( 0 )
77+
78+ WorkoutListView ( )
79+ . tag ( 1 )
80+
81+ NowPlayingView ( )
82+ . tag ( 2 )
83+ }
84+ . tabViewStyle ( . page)
85+ . onAppear {
86+ workoutManager. requestAuthorization ( )
87+ }
88+ }
89+ }
90+
91+ struct WorkoutListView : View {
492 @StateObject var viewModel = WatchViewModel ( )
593
694 var body : some View {
@@ -18,7 +106,7 @@ struct ContentView: View {
18106 }
19107 . navigationTitle ( " Today " )
20108 . overlay {
21- if viewModel. activities. isEmpty {
109+ if viewModel. activities. isEmpty && false {
22110 Text ( " Open iPhone app to sync today's workout. " )
23111 . multilineTextAlignment ( . center)
24112 . padding ( )
@@ -36,40 +124,53 @@ struct ActivityDetailView: View {
36124 @State private var weight : Double = 0.0
37125 @State private var initialized = false
38126
127+ @State private var buttonFeedbackToggle = false
128+
39129 var activity : WatchActivity ? {
40130 viewModel. activities. first ( where: { $0. id == activityId } )
41131 }
42132
43133 var body : some View {
44134 ScrollView {
45135 if let act = activity {
46- VStack ( spacing: 16 ) {
47- Text ( act. name)
48- . font ( . headline)
49- . multilineTextAlignment ( . center)
50-
51- HStack {
52- Text ( " Reps: " )
53- Spacer ( )
54- Stepper ( " \( reps) " , value: $reps, in: 1 ... 100 )
55- }
56-
57- if act. type == " Weighted " {
58- HStack {
59- Text ( " Weight: " )
60- Spacer ( )
61- Stepper ( String ( format: " %.1f " , weight) , value: $weight, in: 0 ... 500 , step: 2.5 )
136+
137+ VStack ( spacing: 8 ) {
138+ Text ( act. name)
139+ . font ( . headline)
140+ . multilineTextAlignment ( . center)
141+ if act. type == " Weighted " {
142+ Text ( " Previous: \( act. previousReps) reps @ \( String ( format: " %.0f " , act. previousWeight) ) " )
143+ . font ( . subheadline)
144+ . foregroundColor ( . secondary)
145+ } else {
146+ Text ( " Previous: \( act. previousReps) reps " )
147+ . font ( . subheadline)
148+ . foregroundColor ( . secondary)
62149 }
150+
151+ VStack {
152+ Text ( " Reps: " )
153+ Stepper ( " \( reps) " , value: $reps, in: 1 ... 100 )
154+ }
155+
156+ if act. type == " Weighted " {
157+ VStack {
158+ Text ( " Weight: " )
159+ Stepper ( String ( format: " %.0f " , weight) , value: $weight, in: 0 ... 500 , step: 5.0 )
160+ }
161+ }
162+
163+ Button ( action: {
164+ viewModel. logSet ( for: act. id, reps: reps, weight: weight)
165+ buttonFeedbackToggle. toggle ( )
166+ } ) {
167+ Text ( " Log Set ( \( act. completedSets) ) " )
168+ . fontWeight ( . bold)
169+ }
170+ . tint ( . blue)
171+ . sensoryFeedback ( . success, trigger: buttonFeedbackToggle)
63172 }
64-
65- Button ( action: {
66- viewModel. logSet ( for: act. id, reps: reps, weight: weight)
67- } ) {
68- Text ( " Log Set ( \( act. completedSets) ) " )
69- . fontWeight ( . bold)
70- }
71- . tint ( . blue)
72- }
173+
73174 . padding ( )
74175 . onAppear {
75176 if !initialized {
0 commit comments