- WWDC22) Build a workout app for Apple Watch ์ธ์ ์์ ์์๋ณด๋ HealthKit ์ ๋ํด์ ๊ธฐ๋กํด๋ณด์์ต๋๋ค.
- HealthKit ์ heart rate ๋ฅผ ์ฌ์ฉํ๊ธฐ ์ํ ๋ชฉํ๋ฅผ ๊ฐ์ง๊ณ ์์ฒญํ ์ธ์ ์ ๋๋ค.
๋ค์์ ์ธ์ ์ ๊ฒฐ๊ณผ์ ๋๋ค.
์ธ์
์ค ์ฌ๋ฐ์๋ฅผ ์์งํ ์ ์๋ HKWorkoutSession
ํด๋์ค์ ๋ํด์ ๋ค์ ์ ์์๋ค.
HKWorkoutSession
์ ๋ฐ์ดํฐ ์์ง์ ์ํด ์ฅ์น์ ์ผ์๋ฅผ ์ค๋นํ๋ฏ๋ก, ์ด๋๊ณผ ๊ด๋ จ๋ ๋ฐ์ดํฐ๋ฅผ ์ ํํ๊ฒ ์์งํ ์ ์์ต๋๋ค (์นผ๋ก๋ฆฌ์ ์ฌ๋ฐ์์ ๊ฐ์ ์ ๋ณด ์์ง) . ๋ํ ์ด๋์ด ํ์ฑํ๋์ด ์์ ๋ ์ ํ๋ฆฌ์ผ์ด์ ์ด ๋ฐฑ๊ทธ๋ผ์ด๋์์ ์คํ๋๋๋ก ํฉ๋๋ค.HKLiveWorkoutBuilder
๋ HKWorkout ๊ฐ์ฒด๋ฅผ ์์ฑํ๊ณ ์ ์ฅํฉ๋๋ค. ์๋์ผ๋ก ์ํ๊ณผ ์ด๋ฒคํธ๋ฅผ ์์งํฉ๋๋ค.
**New ways to work with workouts**
์ธ์
์์ ๋ง์ ๋ด์ฉ์ ํ์ธํ ์ ์์ต๋๋ค.
/// Request authorization to access HealthKit.
func requestAuthorization() {
// The quantity type to write to the health store.
let typesToShare: Set = [
HKQuantityType.workoutType()
]
// The quantity types to read from the health store.
let typesToRead: Set = [
HKQuantityType.quantityType(forIdentifier: .heartRate)!,
HKQuantityType.quantityType(forIdentifier: .activeEnergyBurned)!,
HKQuantityType.quantityType(forIdentifier: .distanceWalkingRunning)!,
HKQuantityType.quantityType(forIdentifier: .distanceCycling)!,
HKObjectType.activitySummaryType()
]
// Request authorization for those quantity types.
healthStore.requestAuthorization(toShare: typesToShare, read: typesToRead) { (success, error) in
// Handle error.
}
}
// โ
๋ทฐ๊ฐ ๋ํ๋ ๋ ๊ถํ์ ์์ฒญํ๋ฉด ๋ฉ๋๋ค.
// .onAppear {
// workoutManager.requestAuthorization()
// }
- watch app target ์
HealthKit
์ ์ถ๊ฐํฉ๋๋ค. - ๋ฐฑ๊ทธ๋ผ์ด๋์์ workout session ์ด ์คํ๋์ด์ผ ํ๊ธฐ ๋๋ฌธ์
background modes capability
๋ฅผ ์ถ๊ฐํด์ผ ํฉ๋๋ค.
- ํด๋น target ์ด HealthKit ์ ์ง์ํ๋๋ก ์ค์ ํฉ๋๋ค.
- apple developer ์์ Identifiers ์์ App ID ๋ก ์ถ๊ฐํฉ๋๋ค.
- HealthKit ์ง์์ ์ ํํ๊ณ ๋ฒ๋ค ์์ด๋๋ฅผ ์ ๋ ฅํด์ App ID ๋ฅผ ์์ฑํฉ๋๋ค.
- info.plist ์์ ๊ถํ์ ์ป์ ๋ ๋ฌธ๊ตฌ๋ฅผ ์ค์ ํด์ค๋๋ค.
NSHealthShareUsageDescription
ํน์Privacy - Health Share Usage Description
key ๋ฅผ ์ถ๊ฐํด์ค๋๋ค.(์ฑ์ด ์ ๋ฐ์ดํฐ๋ฅผ ์ฝ์ด์์ผ ํ๋์ง ์ค๋ช ํฉ๋๋ค.)NSHealthUpdateUsageDescription
ํน์Privacy - Health Update Usage Description
key ๋ฅผ ์ถ๊ฐํด์ค๋๋ค.(์ฑ์์ ์์ฑํ๋ ค๋ ๋ฐ์ดํฐ์ ๋ํด์ ์ค๋ช ํฉ๋๋ค.)
- ๋น๋ ํด๋ณด๊ฒ ์ต๋๋ค. (
์ฌ์ฅ - ์ฌ๋ฐ์
์ ์ฝ๊ธฐ ๊ถํ์ ์ป์ ์ ์์ต๋๋ค.)- ์ฐ๋ฆฌ๊ฐ ์ ๊ณตํ ์ค๋ช ๋ค์ ๋ณผ ์ ์์ต๋๋ค.
- ์ฐ๋ฆฌ๊ฐ ์ถ๊ฐํ ๊ถํ์ ๋ํด์ ๋ณผ ์ ์์ต๋๋ค.
๊ถํ์ ์ป์์ผ๋ ์ฌ๋ฐ์์ ์ ๋ณด๋ฅผ ์ป์ด๋ณด๊ฒ ์ต๋๋ค.
- builder ์๋ก์ด ์ํ์ ์์งํ ๋๋ง๋ค ํธ์ถ๋๋ delegate method ๋ฅผ ๋ค์๊ณผ ๊ฐ์ด ๊ตฌํํ์์ต๋๋ค.
- statistics ์ค heartRate ์ ํด๋นํ๋ ๊ฒฝ์ฐ๋ฅผ switch ๋ฌธ์ผ๋ก ๋์ํด์ฃผ์์ต๋๋ค.
// MARK: - HKLiveWorkoutBuilderDelegate
extension WorkoutManager: HKLiveWorkoutBuilderDelegate {
/// called whenever the builder collects an events.
func workoutBuilderDidCollectEvent(_ workoutBuilder: HKLiveWorkoutBuilder) {
// ...
}
/// โ
called whenever the builder collects new samples.
func workoutBuilder(_ workoutBuilder: HKLiveWorkoutBuilder, didCollectDataOf collectedTypes: Set<HKSampleType>) {
for type in collectedTypes {
guard let quantityType = type as? HKQuantityType else { return }
let statistics = workoutBuilder.statistics(for: quantityType)
// โ
Update the published values.
updateForStatistics(statistics)
}
}
}
// ...
// MARK: - Workout Metrics
@Published var averageHeartRate: Double = 0
@Published var heartRate: Double = 0
func updateForStatistics(_ statistics: HKStatistics?) {
guard let statistics = statistics else { return }
DispatchQueue.main.async {
switch statistics.quantityType {
// โ
We want beats per minute, so we use a count HKUnit divided by a minute HKUnit.
case HKQuantityType.quantityType(forIdentifier: .heartRate):
let heartRateUnit = HKUnit.count().unitDivided(by: HKUnit.minute())
self.heartRate = statistics.mostRecentQuantity()?.doubleValue(for: heartRateUnit) ?? 0
self.averageHeartRate = statistics.averageQuantity()?.doubleValue(for: heartRateUnit) ?? 0
// ...
}
}
}
data flow ์ ๋ํด์ ์์๋ณด์.
WorkoutManager(์ฐ๋ฆฌ๊ฐ ๋ง๋๋ HealthKit ์ ๋งค๋์ง ํ ์ ์๋ ํด๋์ค)๋ HealthKit ๊ณผ์ ์ธํฐํ์ด์ค๋ฅผ ๋ด๋นํฉ๋๋ค. HKWorkoutSession ๊ณผ ์ธํฐํ์ด์คํ์ฌ ์ด๋์ ์์, ์ผ์ ์ค์ง ๋ฐ ์ข ๋ฃ ํ ์ ์์ต๋๋ค. HKLiveWorkoutBuilder ์ ์ธํฐํ์ด์คํ์ฌ ์ด๋ ์ํ์ ์์ ํ๊ณ ํด๋น ๋ฐ์ดํฐ๋ฅผ ๋ทฐ์ ์ ๊ณตํ ์ ์์ต๋๋ค.
environmentObject ๋ก WorkoutManager ๋ฅผ ํ ๋นํ์ฌ NavigationView ์ ๋ทฐ ๊ณ์ธต ๊ตฌ์กฐ์ ์๋ ๋ทฐ์ ์ ํํ ์ ์์ต๋๋ค. ๊ทธ๋ฆฌ๊ณ ๋ค์ ๋ทฐ๋ @EnvironmentObject ๋ฅผ ์ ์ธํ์ฌ WorkoutManager ์ ๋ํ ์ก์ธ์ค ๊ถํ์ ์ป์ ์ ์์ต๋๋ค.
์ถ์ฒ:
Build a workout app for Apple Watch - WWDC21 - Videos - Apple Developer
Apple Developer Documentation - Testing custom notification interfaces
์๋ Notification interfaces ๋ฅผ ๊ธฐ๊ธฐ์์ ๋น๋ํ๋ ๋ฐฉ๋ฒ์ ์๊ฐํด์ค ๊ธ์ ๋๋ค. ์ด๋ฅผ ํตํด ์ฐ๋ฆฌ๋ ์๋ชฉ์ ์ฐฉ์ฉํ์ง ์์๋ ์ด๋ค ์กฐ๊ฑด์ผ๋ก ๊ธฐ๊ธฐ์์ ๋น๋ํ ์ ์๋์ง ์ ์ ์์ต๋๋ค.
๊ธฐ๊ธฐ๋ฅผ ์๋ชฉ์ ์ฐฉ์ฉํ์ง ์์ ์ํ์์ notification interfaces ๋ฅผ ํ ์คํธํ๋ ค๋ฉด ๋ค์์ ๋จ๊ณ๋ฅผ ๋ฐ๋ฅด๋ฉด ๋ฉ๋๋ค.
- ์ ํ ์์น์์ ์๋ชฉ ๊ฐ์ง๋ฅผ ๋นํ์ฑํ ํฉ๋๋ค. companion iPhone ์ watch ์ฑ ๋๋ watch ์ Setting ์์ ์ค์ ํ ์ ์์ต๋๋ค. ์ต์ ์ Passcode > Wrist Detection ์ ์์ต๋๋ค.
- ์ ํ ์์น๊ฐ ์ถฉ์ ๊ธฐ์ ์ฐ๊ฒฐ๋์ด ์์ง ์์์ง ํ์ธํฉ๋๋ค.
- iPhone ์ ์ ๊ธ๋๋ค.
watch-only app ์ ๋ง๋ค๋๋ผ๋ ๋ด iPhone ๊ธฐ๊ธฐ๋ฅผ ํตํด์ ์ ํ์์น์ ์ ๊ทผํ ์ ์์์ต๋๋ค.
์ด๋ provisioning profile ์ ์ ํ ์์น ๊ธฐ๊ธฐ๋ฅผ ์ถ๊ฐํ์ฌ ๋น๋ํ ์ ์๋ค๋ ์ฐฝ์ด ๋ฑ์ฅํฉ๋๋ค. ์ด๋ฅผ ํตํด ์ ๋ขฐํ๊ณ ๋น๋ํ ์ ์๋ ๊ธฐ๊ธฐ๋ก ์ถ๊ฐํ ์ ์์์ต๋๋ค.