Skip to content

Commit 97d03b5

Browse files
committed
basic apple watch integration working
1 parent 3e378da commit 97d03b5

9 files changed

Lines changed: 421 additions & 108 deletions

File tree

check_targets.rb

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
require 'xcodeproj'
2+
project = Xcodeproj::Project.open('ios/Runner.xcodeproj')
3+
project.targets.each do |target|
4+
target.source_build_phase.files.each do |file|
5+
if file.file_ref && file.file_ref.path =~ /ContentView/
6+
puts "#{target.name} compiles #{file.file_ref.path}"
7+
end
8+
if file.file_ref && file.file_ref.path =~ /WatchViewModel/
9+
puts "#{target.name} compiles #{file.file_ref.path}"
10+
end
11+
end
12+
end

fix_watch_project.rb

Lines changed: 0 additions & 27 deletions
This file was deleted.

ios/NeverSkipWatch Watch App/ContentView.swift

Lines changed: 127 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,94 @@
11
import 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

360
struct 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 {
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3+
<plist version="1.0">
4+
<dict>
5+
<key>com.apple.security.application-groups</key>
6+
<array>
7+
<string>group.io.hawkford.fredericapp</string>
8+
</array>
9+
</dict>
10+
</plist>

ios/NeverSkipWatch Watch App/WatchViewModel.swift

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ class WatchViewModel: NSObject, ObservableObject, WCSessionDelegate {
99
super.init()
1010
loadActivities()
1111

12+
// activities = [WatchActivity(id: "darm", name: "Underhand Dumbbell Bench Press", targetSets: 3, targetReps: 10, previousWeight: 40, previousReps: 8, type: "Weighted", completedSets: 1),
13+
// WatchActivity(id: "darm2", name: "Explosive Bulgarian Split Squat", targetSets: 3, targetReps: 10, previousWeight: 40, previousReps: 8, type: "Weighted", completedSets: 1)]
14+
1215
if WCSession.isSupported() {
1316
WCSession.default.delegate = self
1417
WCSession.default.activate()
@@ -47,9 +50,9 @@ class WatchViewModel: NSObject, ObservableObject, WCSessionDelegate {
4750
}
4851
}
4952

50-
func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {}
53+
nonisolated func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {}
5154

52-
func session(_ session: WCSession, didReceiveApplicationContext applicationContext: [String : Any]) {
55+
nonisolated func session(_ session: WCSession, didReceiveApplicationContext applicationContext: [String : Any]) {
5356
if let acts = applicationContext["activities"] as? [[String: Any]] {
5457
var newActivities: [WatchActivity] = []
5558
for act in acts {
@@ -66,9 +69,9 @@ class WatchViewModel: NSObject, ObservableObject, WCSessionDelegate {
6669
}
6770
}
6871

69-
DispatchQueue.main.async {
70-
self.activities = newActivities
71-
self.saveActivities()
72+
Task { @MainActor [weak self] in
73+
self?.activities = newActivities
74+
self?.saveActivities()
7275
}
7376
}
7477
}

0 commit comments

Comments
 (0)