Skip to content

Commit 99eedec

Browse files
authored
Merge pull request #276 from thingineeer/feature/v2.3.2-watch-sync
feat: Apple Watch 실시간 러닝 연동 및 심박수 존 시각화
2 parents 384429f + 9e2f2f4 commit 99eedec

File tree

30 files changed

+1002
-436
lines changed

30 files changed

+1002
-436
lines changed

Runnect-iOS/Gemfile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
source "https://rubygems.org"
22

33
gem "fastlane"
4-
gem 'cocoapods', '1.15.0'
5-
gem 'rexml', '~> 3.2.4'
4+
gem 'cocoapods', '1.16.2'
5+
gem 'rexml'

Runnect-iOS/Podfile.lock

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1198,8 +1198,6 @@ PODS:
11981198
- FirebaseCore (= 11.5)
11991199
- FirebaseCoreInternal (11.5.0):
12001200
- "GoogleUtilities/NSData+zlib (~> 8.0)"
1201-
- FirebaseDynamicLinks (11.5.0):
1202-
- FirebaseCore (= 11.5)
12031201
- FirebaseFirestore (11.5.0):
12041202
- FirebaseCore (= 11.5)
12051203
- FirebaseCoreExtension (= 11.5)
@@ -1417,7 +1415,6 @@ DEPENDENCIES:
14171415
- Firebase/RemoteConfig
14181416
- FirebaseAnalytics
14191417
- FirebaseAuth
1420-
- FirebaseDynamicLinks
14211418
- FirebaseFirestore
14221419
- Google-Mobile-Ads-SDK
14231420
- KakaoSDKAuth
@@ -1446,7 +1443,6 @@ SPEC REPOS:
14461443
- FirebaseCore
14471444
- FirebaseCoreExtension
14481445
- FirebaseCoreInternal
1449-
- FirebaseDynamicLinks
14501446
- FirebaseFirestore
14511447
- FirebaseFirestoreInternal
14521448
- FirebaseInstallations
@@ -1500,7 +1496,6 @@ SPEC CHECKSUMS:
15001496
FirebaseCore: 93abc05437f8064cd2bc0a53b768fb0bc5a1d006
15011497
FirebaseCoreExtension: ddb2eb987f736b714d30f6386795b52c4670439e
15021498
FirebaseCoreInternal: f47dd28ae7782e6a4738aad3106071a8fe0af604
1503-
FirebaseDynamicLinks: 0e4954b3d050560dfa4bf3a7e8ffe16c9a4b8280
15041499
FirebaseFirestore: 3f59cb7b6f62b362886743d4c92e83a66b5e0a5d
15051500
FirebaseFirestoreInternal: d8d71a7f27834573404834172886183f1cd48c3d
15061501
FirebaseInstallations: d8063d302a426d114ac531cd82b1e335a0565745
@@ -1529,6 +1524,6 @@ SPEC CHECKSUMS:
15291524
SnapKit: e01d52ebb8ddbc333eefe2132acf85c8227d9c25
15301525
Then: 844265ae87834bbe1147d91d5d41a404da2ec27d
15311526

1532-
PODFILE CHECKSUM: 26bfd41a8069c7b123fa093a3210e1b1ca878377
1527+
PODFILE CHECKSUM: 60ec206c7be64e5888b1a76566274442b7aef025
15331528

15341529
COCOAPODS: 1.16.2

Runnect-iOS/RNWatch Watch App/Assets.xcassets/AppIcon.appiconset/Contents.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
{
22
"images" : [
33
{
4+
"filename" : "runnect logo-01 1.png",
45
"idiom" : "universal",
56
"platform" : "watchos",
67
"size" : "1024x1024"
37.1 KB
Loading

Runnect-iOS/RNWatch Watch App/Connectivity/WatchSessionManager.swift

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -183,11 +183,20 @@ extension WatchSessionManager: WCSessionDelegate {
183183
}
184184
}
185185

186-
case "runStarted":
186+
case "countdownStarted":
187187
runningState = .countdown
188188
lastKilometer = 0
189189
resetLocalTimer()
190190

191+
case "runStarted":
192+
// Fallback: if countdown message was missed, go directly to active
193+
if runningState != .countdown {
194+
lastKilometer = 0
195+
resetLocalTimer()
196+
}
197+
runningState = .active
198+
startLocalTimer()
199+
191200
case "runCompleted":
192201
runningState = .summary
193202
stopLocalTimer()
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
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>CFBundleDevelopmentRegion</key>
6+
<string>$(DEVELOPMENT_LANGUAGE)</string>
7+
<key>CFBundleDisplayName</key>
8+
<string>Runnect</string>
9+
<key>CFBundleExecutable</key>
10+
<string>$(EXECUTABLE_NAME)</string>
11+
<key>CFBundleIdentifier</key>
12+
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
13+
<key>CFBundleInfoDictionaryVersion</key>
14+
<string>6.0</string>
15+
<key>CFBundleName</key>
16+
<string>$(PRODUCT_NAME)</string>
17+
<key>CFBundlePackageType</key>
18+
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
19+
<key>CFBundleShortVersionString</key>
20+
<string>2.4.0</string>
21+
<key>CFBundleVersion</key>
22+
<string>2026.0210.0051</string>
23+
<key>NSHealthShareUsageDescription</key>
24+
<string>러닝 중 심박수와 칼로리 정보를 표시하기 위해 건강 데이터 접근이 필요합니다.</string>
25+
<key>NSHealthUpdateUsageDescription</key>
26+
<string>러닝 운동 기록을 건강 앱에 저장하기 위해 건강 데이터 쓰기 권한이 필요합니다.</string>
27+
<key>UIUserInterfaceStyle</key>
28+
<string>Dark</string>
29+
<key>WKApplication</key>
30+
<true/>
31+
<key>WKCompanionAppBundleIdentifier</key>
32+
<string>com.runnect.Runnect-iOS</string>
33+
</dict>
34+
</plist>
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
//
2+
// HeartRateZone.swift
3+
// RNWatch Watch App
4+
//
5+
// Created by Runnect on 2026/02/09.
6+
//
7+
8+
import SwiftUI
9+
10+
enum HeartRateZone: Int, CaseIterable {
11+
case zone1 = 1 // 워밍업 50-60%
12+
case zone2 = 2 // 지방 연소 60-70%
13+
case zone3 = 3 // 유산소 70-80%
14+
case zone4 = 4 // 젖산 역치 80-90%
15+
case zone5 = 5 // 최대 90-100%
16+
17+
var name: String {
18+
switch self {
19+
case .zone1: return "워밍업"
20+
case .zone2: return "지방 연소"
21+
case .zone3: return "유산소"
22+
case .zone4: return "젖산 역치"
23+
case .zone5: return "최대"
24+
}
25+
}
26+
27+
var color: Color {
28+
switch self {
29+
case .zone1: return Color(red: 0x3B / 255, green: 0x82 / 255, blue: 0xF6 / 255)
30+
case .zone2: return Color(red: 0x22 / 255, green: 0xC5 / 255, blue: 0x5E / 255)
31+
case .zone3: return Color(red: 0xEA / 255, green: 0xB3 / 255, blue: 0x08 / 255)
32+
case .zone4: return Color(red: 0xF9 / 255, green: 0x73 / 255, blue: 0x16 / 255)
33+
case .zone5: return Color(red: 0xEF / 255, green: 0x44 / 255, blue: 0x44 / 255)
34+
}
35+
}
36+
37+
var shortLabel: String {
38+
"Z\(rawValue)"
39+
}
40+
41+
var range: ClosedRange<Double> {
42+
switch self {
43+
case .zone1: return 0.50...0.60
44+
case .zone2: return 0.60...0.70
45+
case .zone3: return 0.70...0.80
46+
case .zone4: return 0.80...0.90
47+
case .zone5: return 0.90...1.00
48+
}
49+
}
50+
51+
static func zone(for heartRate: Double, maxHeartRate: Double = 190) -> HeartRateZone {
52+
guard maxHeartRate > 0 else { return .zone1 }
53+
let ratio = heartRate / maxHeartRate
54+
55+
switch ratio {
56+
case ..<0.60: return .zone1
57+
case 0.60..<0.70: return .zone2
58+
case 0.70..<0.80: return .zone3
59+
case 0.80..<0.90: return .zone4
60+
default: return .zone5
61+
}
62+
}
63+
}

Runnect-iOS/RNWatch Watch App/RNWatch Watch App.entitlements

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,5 @@
44
<dict>
55
<key>com.apple.developer.healthkit</key>
66
<true/>
7-
<key>com.apple.developer.healthkit.access</key>
8-
<array/>
97
</dict>
108
</plist>

Runnect-iOS/RNWatch Watch App/Utils/HapticManager.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,8 @@ enum HapticManager {
2323
static func runCompleted() {
2424
WKInterfaceDevice.current().play(.success)
2525
}
26+
27+
static func heartRateZoneAlert() {
28+
WKInterfaceDevice.current().play(.notification)
29+
}
2630
}

Runnect-iOS/RNWatch Watch App/Views/ActiveRunView.swift

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,12 @@ struct ActiveRunView: View {
3939

4040
// Course progress bar
4141
if sessionManager.runningData.totalCourseDistance > 0 {
42-
ProgressView(value: sessionManager.runningData.progress)
43-
.progressViewStyle(.linear)
44-
.tint(Color.runnectPrimary)
45-
.frame(height: 4)
46-
.padding(.horizontal, 8)
42+
CourseProgressBar(
43+
progress: sessionManager.runningData.progress,
44+
currentDistance: sessionManager.runningData.distance,
45+
totalDistance: sessionManager.runningData.totalCourseDistance
46+
)
47+
.padding(.horizontal, 8)
4748
}
4849

4950
// Time and Pace row
@@ -79,11 +80,20 @@ struct ActiveRunView: View {
7980
HStack(spacing: 4) {
8081
Image(systemName: "heart.fill")
8182
.font(.system(size: 10))
82-
.foregroundColor(.runnectHeartRate)
83+
.foregroundColor(heartRateZoneColor)
8384

8485
Text(heartRateText)
8586
.font(.system(size: 14, weight: .medium, design: .rounded))
86-
.foregroundColor(.runnectHeartRate)
87+
.foregroundColor(heartRateZoneColor)
88+
89+
if workoutManager.heartRate > 0 {
90+
Text(workoutManager.currentZone.shortLabel)
91+
.font(.system(size: 8, weight: .bold, design: .rounded))
92+
.foregroundColor(.white)
93+
.padding(.horizontal, 4)
94+
.padding(.vertical, 1)
95+
.background(Capsule().fill(workoutManager.currentZone.color))
96+
}
8797

8898
Spacer()
8999

@@ -166,6 +176,10 @@ struct ActiveRunView: View {
166176
}
167177
}
168178

179+
private var heartRateZoneColor: Color {
180+
workoutManager.heartRate > 0 ? workoutManager.currentZone.color : .runnectHeartRate
181+
}
182+
169183
private var heartRateText: String {
170184
let hr = Int(workoutManager.heartRate)
171185
return hr > 0 ? "\(hr)" : "--"

0 commit comments

Comments
 (0)