Ver3.0
์ ์ฒด ์ด๋
Ver3.0 ๐ฅ HealthKit๊ณผ CareKit์ผ๋ก ๋ง๋๋ ํฌ์ค์ผ์ด ์ฑ

๐ฅ HealthKit๊ณผ CareKit์ผ๋ก ๋ง๋๋ ํฌ์ค์ผ์ด ์ฑ
์ ํ ์ํ๊ณ์์ ๊ฑด๊ฐ ๋ฐ์ดํฐ๋ฅผ ๋ค๋ฃจ๋ ์๋ฒฝ ๊ฐ์ด๋
๐ฏ ์ ์ง๊ธ ํฌ์ค์ผ์ด ์ฑ์ธ๊ฐ?
์์ฆ ์ฃผ๋ณ์ ๋๋ฌ๋ณด๋ฉด ์๋ชฉ์ ์ ํ์์น๋ฅผ ์ฐจ๊ณ ์๋ ์ฌ๋๋ค์ด ์ ๋ง ๋ง์์ก์ง? ๐โ ๊ทธ๋ฅ ์๊ฐ๋ง ๋ณด๋ ๊ฒ ์๋๋ผ ์ฌ๋ฐ์, ๊ฑธ์ ์, ์๋ฉด ํจํด๊น์ง ์ฒดํฌํ๋ฉด์ ์๊ธฐ ๊ฑด๊ฐ์ ๊ด๋ฆฌํ๋ ์๋๊ฐ ๋์ด. ์ด๋ฐ ํ๋ฆ ์์์ ํฌ์ค์ผ์ด ์ฑ ๊ฐ๋ฐ์ ๋จ์ํ ํธ๋ ๋๊ฐ ์๋๋ผ ํ์ ์ญ๋์ด ๋์ด๊ฐ๊ณ ์์ด.์ ํ์ ์ด๋ฏธ ์ค๋์ ๋ถํฐ ๊ฑด๊ฐ ๋ฐ์ดํฐ์ ์ค์์ฑ์ ์๊ณ ์์๊ณ , ๊ทธ๋์ ๋ง๋ ๊ฒ ๋ฐ๋ก HealthKit๊ณผ CareKit์ด์ผ. ์ด ๋ ํ๋ ์์ํฌ๋ iOS ์ํ๊ณ์์ ๊ฑด๊ฐ ๊ด๋ จ ์ฑ์ ๋ง๋ค ๋ ์ ๋ ๋นผ๋์ ์ ์๋ ํต์ฌ ๋๊ตฌ๋ค์ด์ง. ๐ช
ํนํ ์ฌ๋ฅ๋ท ๊ฐ์ ํ๋ซํผ์์ ํฌ์ค์ผ์ด ์ฑ ๊ฐ๋ฐ ์ฌ๋ฅ์ ๊ณต์ ํ๊ฑฐ๋ ๋ฐฐ์ฐ๋ ค๋ ์ฌ๋๋ค์ด ๋์ด๋๊ณ ์๋๋ฐ, ๊ทธ๋งํผ ์์ฅ์ ์์๊ฐ ํฌ๋ค๋ ๋ป์ด๊ธฐ๋ ํด. ์, ๊ทธ๋ผ ๋ณธ๊ฒฉ์ ์ผ๋ก ์ด ๋ ํ๋ ์์ํฌ๊ฐ ๋ญ์ง, ์ด๋ป๊ฒ ํ์ฉํ๋์ง ์น๊ตฌ์ฒ๋ผ ํธํ๊ฒ ์์๋ณด์! ๐
๐ฑ HealthKit์ด ๋ญ๊ธธ๋?
HealthKit์ ์ ์ฒด
HealthKit์ 2014๋ iOS 8๊ณผ ํจ๊ป ์ฒ์ ๋ฑ์ฅํ ํ๋ ์์ํฌ์ผ. ๊ฐ๋จํ ๋งํ๋ฉด ์์ดํฐ๊ณผ ์ ํ์์น์ ์ ์ฅ๋ ๊ฑด๊ฐ ๋ฐ์ดํฐ๋ฅผ ์์ ํ๊ฒ ์ฝ๊ณ ์ธ ์ ์๊ฒ ํด์ฃผ๋ ์ค์ ์ ์ฅ์๋ผ๊ณ ์๊ฐํ๋ฉด ๋ผ. ๐ฆ์๋ฅผ ๋ค์ด๋ณผ๊ฒ. ๋๊ฐ ์์นจ์ ์กฐ๊น ์ ํ๋ฉด์ ๋์ดํค ๋ฌ๋ ์ฑ์ ์ฌ์ฉํ์ด. ๊ทธ๋ผ ์ด ์ฑ์ HealthKit์ ๋์ ์ด๋ ๋ฐ์ดํฐ๋ฅผ ์ ์ฅํด. ๊ทธ๋ฆฌ๊ณ ์ ์ฌ๋ ๋ค๋ฅธ ๊ฑด๊ฐ ๊ด๋ฆฌ ์ฑ์ ์ด๋ฉด, ๊ทธ ์ฑ๋ HealthKit์์ ์์นจ ์ด๋ ๋ฐ์ดํฐ๋ฅผ ์ฝ์ด์ฌ ์ ์์ง. ์ด๋ ๊ฒ ์ฌ๋ฌ ์ฑ๋ค์ด ํ๋์ ๊ฑด๊ฐ ๋ฐ์ดํฐ ์ ์ฅ์๋ฅผ ๊ณต์ ํ๋ ๊ฑฐ์ผ. ๐
๐ฏ HealthKit์ ํต์ฌ ๊ธฐ๋ฅ
1. ๋ฐ์ดํฐ ํตํฉ - ์ฌ๋ฌ ์ฑ๊ณผ ๊ธฐ๊ธฐ์ ๊ฑด๊ฐ ๋ฐ์ดํฐ๋ฅผ ํ๊ณณ์ ๋ชจ์
2. ํ๋ผ์ด๋ฒ์ ๋ณดํธ - ์ฌ์ฉ์๊ฐ ์ง์ ์ด๋ค ๋ฐ์ดํฐ๋ฅผ ๊ณต์ ํ ์ง ์ ํ
3. ์ค์๊ฐ ๋๊ธฐํ - ์ ํ์์น, ์์ดํฐ, ์์ดํจ๋ ๊ฐ ์๋ ๋๊ธฐํ
4. ๋ฐฑ๊ทธ๋ผ์ด๋ ์ ๋ฐ์ดํธ - ์ฑ์ด ๊บผ์ ธ์์ด๋ ๋ฐ์ดํฐ ์์ง ๊ฐ๋ฅ
1. ๋ฐ์ดํฐ ํตํฉ - ์ฌ๋ฌ ์ฑ๊ณผ ๊ธฐ๊ธฐ์ ๊ฑด๊ฐ ๋ฐ์ดํฐ๋ฅผ ํ๊ณณ์ ๋ชจ์
2. ํ๋ผ์ด๋ฒ์ ๋ณดํธ - ์ฌ์ฉ์๊ฐ ์ง์ ์ด๋ค ๋ฐ์ดํฐ๋ฅผ ๊ณต์ ํ ์ง ์ ํ
3. ์ค์๊ฐ ๋๊ธฐํ - ์ ํ์์น, ์์ดํฐ, ์์ดํจ๋ ๊ฐ ์๋ ๋๊ธฐํ
4. ๋ฐฑ๊ทธ๋ผ์ด๋ ์ ๋ฐ์ดํธ - ์ฑ์ด ๊บผ์ ธ์์ด๋ ๋ฐ์ดํฐ ์์ง ๊ฐ๋ฅ
HealthKit์ด ๋ค๋ฃจ๋ ๋ฐ์ดํฐ ์ข ๋ฅ
HealthKit์ด ๊ด๋ฆฌํ๋ ๊ฑด๊ฐ ๋ฐ์ดํฐ๋ ์ ๋ง ๋ค์ํด. ํฌ๊ฒ ๋ช ๊ฐ์ง ์นดํ ๊ณ ๋ฆฌ๋ก ๋๋ ์ ์์ด:๐ ์ ์ฒด ์ธก์ ๋ฐ์ดํฐ (Body Measurements)
ํค, ๋ชธ๋ฌด๊ฒ, ์ฒด์ง๋์ง์(BMI), ์ฒด์ง๋ฐฉ๋ฅ ๊ฐ์ ๊ธฐ๋ณธ์ ์ธ ์ ์ฒด ์ ๋ณด๋ค์ด์ผ. ์ด๋ฐ ๋ฐ์ดํฐ๋ ๋ณดํต ์๋์ผ๋ก ์ ๋ ฅํ๊ฑฐ๋ ์ค๋งํธ ์ฒด์ค๊ณ ๊ฐ์ ๊ธฐ๊ธฐ์์ ์๋์ผ๋ก ๋ฐ์์ค์ง.
๐ ํ๋ ฅ ์งํ (Vital Signs)
์ฌ๋ฐ์, ํ์, ์ฒด์จ, ํธํก์, ํ์ค ์ฐ์ํฌํ๋ ๋ฑ ์๋ช ๊ณผ ์ง๊ฒฐ๋ ์ค์ํ ๋ฐ์ดํฐ๋ค์ด์ผ. ์ ํ์์น๊ฐ ํนํ ์ด ๋ถ๋ถ์์ ๊ฐ๋ ฅํ๋ฐ, ์ฌ๋ฐ์๋ ๊ฑฐ์ ์ค์๊ฐ์ผ๋ก ์ธก์ ๋๊ฑฐ๋ . ๐ซ
๐ ํ๋ ๋ฐ์ดํฐ (Activity Data)
๊ฑธ์ ์, ์ด๋ ๊ฑฐ๋ฆฌ, ์ค๋ฅธ ์ธต์, ํ๋ ์นผ๋ก๋ฆฌ, ์ด๋ ์๊ฐ ๋ฑ ์ ์ฒด ํ๋๊ณผ ๊ด๋ จ๋ ๋ชจ๋ ๊ฒ๋ค. ์ด๊ฒ ๋ฐ๋ก ์ ํ์์น์ 'ํ๋' ๋ง์ ์ฑ์ฐ๋ ๋ฐ์ดํฐ์ผ! ๐ฅ
๐ด ์๋ฉด ๋ฐ์ดํฐ (Sleep Analysis)
์๋ฉด ์๊ฐ, ์๋ฉด ๋จ๊ณ(์์ ์๋ฉด, ๊น์ ์๋ฉด, REM ์๋ฉด), ์๋ฉด์ ์ง ๋ฑ์ ์ถ์ ํด. ์์ฆ์ ์๋ฉด ๊ฑด๊ฐ์ด ์ ๋ง ์ค์ํ๊ฒ ์ฌ๊ฒจ์ง์์?
๐ ์์ ๋ฐ์ดํฐ (Nutrition)
์ญ์ทจํ ์นผ๋ก๋ฆฌ, ๋จ๋ฐฑ์ง, ํ์ํ๋ฌผ, ์ง๋ฐฉ, ๋นํ๋ฏผ, ๋ฏธ๋ค๋ ๋ฑ ์์์ ์ ๋ณด. ๋ค์ด์ดํธ ์ฑ์ด๋ ์๋จ ๊ด๋ฆฌ ์ฑ์์ ์ฃผ๋ก ์ฌ์ฉํด.
๐ง ๋ง์์ฑ๊น (Mindfulness)
๋ช ์ ์๊ฐ, ์คํธ๋ ์ค ์์ค ๊ฐ์ ์ ์ ๊ฑด๊ฐ ๊ด๋ จ ๋ฐ์ดํฐ๋ ํฌํจ๋ผ. ์์ฆ ๋ฉํ ํฌ์ค๊ฐ ์ค์ํด์ง๋ฉด์ ์ด ์นดํ ๊ณ ๋ฆฌ๋ ์ฃผ๋ชฉ๋ฐ๊ณ ์์ด. ๐ง
๐ HealthKit์ ํ๋ผ์ด๋ฒ์์ ๋ณด์
๊ฑด๊ฐ ๋ฐ์ดํฐ๋ ์ ๋ง ๋ฏผ๊ฐํ ๊ฐ์ธ์ ๋ณด์์? ๊ทธ๋์ ์ ํ์ HealthKit์ ์์ฒญ๋๊ฒ ๊ฐ๋ ฅํ ๋ณด์ ์์คํ ์ ๊ตฌ์ถํด๋จ์ด. ๐ก๏ธ
๐ก HealthKit ๋ณด์์ ํต์ฌ ์์น
1. ๋ช ์์ ๋์ - ์ฑ์ด ๊ฑด๊ฐ ๋ฐ์ดํฐ์ ์ ๊ทผํ๋ ค๋ฉด ๋ฐ๋์ ์ฌ์ฉ์์ ํ๊ฐ๋ฅผ ๋ฐ์์ผ ํด. ๊ทธ๊ฒ๋ ๋ฐ์ดํฐ ํ์ ๋ณ๋ก ๊ฐ๊ฐ!
2. ์ฝ๊ธฐ/์ฐ๊ธฐ ๊ถํ ๋ถ๋ฆฌ - ๋ฐ์ดํฐ๋ฅผ ์ฝ๋ ๊ถํ๊ณผ ์ฐ๋ ๊ถํ์ด ์์ ํ ๋ถ๋ฆฌ๋์ด ์์ด.
3. ํด๋ผ์ฐ๋ ๋๊ธฐํ ์ํธํ - iCloud๋ฅผ ํตํด ๋๊ธฐํ๋ ๋ ์๋ํฌ์๋ ์ํธํ๊ฐ ์ ์ฉ๋ผ.
4. ์ฑ ์๋๋ฐ์ค - ๊ฐ ์ฑ์ ์๊ธฐ๊ฐ ๋ง๋ ๋ฐ์ดํฐ๋ง ์์ ํ ์ ์๊ณ , ๋ค๋ฅธ ์ฑ์ ๋ฐ์ดํฐ๋ ์ฝ๊ธฐ๋ง ๊ฐ๋ฅํด.
์ค์ ๋ก ์ฑ์ ๊ฐ๋ฐํ ๋ ์ฌ์ฉ์์๊ฒ ๊ถํ์ ์์ฒญํ๋ ๊ณผ์ ์ด ๊ฝค ๊น๋ค๋ก์. ํ์ง๋ง ์ด๊ฒ ๋ฐ๋ก ์ฌ์ฉ์์ ๊ฑด๊ฐ ์ ๋ณด๋ฅผ ์งํค๋ ๋ฐฉ๋ฒ์ด๋๊น ๋น์ฐํ ๊ฑฐ์ง! ๐
1. ๋ช ์์ ๋์ - ์ฑ์ด ๊ฑด๊ฐ ๋ฐ์ดํฐ์ ์ ๊ทผํ๋ ค๋ฉด ๋ฐ๋์ ์ฌ์ฉ์์ ํ๊ฐ๋ฅผ ๋ฐ์์ผ ํด. ๊ทธ๊ฒ๋ ๋ฐ์ดํฐ ํ์ ๋ณ๋ก ๊ฐ๊ฐ!
2. ์ฝ๊ธฐ/์ฐ๊ธฐ ๊ถํ ๋ถ๋ฆฌ - ๋ฐ์ดํฐ๋ฅผ ์ฝ๋ ๊ถํ๊ณผ ์ฐ๋ ๊ถํ์ด ์์ ํ ๋ถ๋ฆฌ๋์ด ์์ด.
3. ํด๋ผ์ฐ๋ ๋๊ธฐํ ์ํธํ - iCloud๋ฅผ ํตํด ๋๊ธฐํ๋ ๋ ์๋ํฌ์๋ ์ํธํ๊ฐ ์ ์ฉ๋ผ.
4. ์ฑ ์๋๋ฐ์ค - ๊ฐ ์ฑ์ ์๊ธฐ๊ฐ ๋ง๋ ๋ฐ์ดํฐ๋ง ์์ ํ ์ ์๊ณ , ๋ค๋ฅธ ์ฑ์ ๋ฐ์ดํฐ๋ ์ฝ๊ธฐ๋ง ๊ฐ๋ฅํด.
๐ป HealthKit ๊ฐ๋ฐ ์์ํ๊ธฐ
๊ฐ๋ฐ ํ๊ฒฝ ์ค์
์, ์ด์ ์ค์ ๋ก HealthKit์ ์ฌ์ฉํด์ ์ฑ์ ๋ง๋ค์ด๋ณด์! ๋จผ์ ๊ฐ๋ฐ ํ๊ฒฝ๋ถํฐ ์ธํ ํด์ผ ํด.
๐ ํ์ํ ๊ฒ๋ค
โข Xcode 14.0 ์ด์
โข iOS 16.0 ์ด์์ ํ๊ฒ์ผ๋ก ํ๋ ํ๋ก์ ํธ
โข ์ ๋ฃ Apple Developer ๊ณ์ (HealthKit์ ์๋ฎฌ๋ ์ดํฐ์์๋ ํ ์คํธ ๊ฐ๋ฅํ์ง๋ง, ์ค์ ๊ธฐ๊ธฐ ํ ์คํธ๋ ์ ๋ฃ ๊ณ์ ํ์)
โข Swift 5.0 ์ด์ (Objective-C๋ ๊ฐ๋ฅํ์ง๋ง Swift ์ถ์ฒ!)
โข Xcode 14.0 ์ด์
โข iOS 16.0 ์ด์์ ํ๊ฒ์ผ๋ก ํ๋ ํ๋ก์ ํธ
โข ์ ๋ฃ Apple Developer ๊ณ์ (HealthKit์ ์๋ฎฌ๋ ์ดํฐ์์๋ ํ ์คํธ ๊ฐ๋ฅํ์ง๋ง, ์ค์ ๊ธฐ๊ธฐ ํ ์คํธ๋ ์ ๋ฃ ๊ณ์ ํ์)
โข Swift 5.0 ์ด์ (Objective-C๋ ๊ฐ๋ฅํ์ง๋ง Swift ์ถ์ฒ!)
ํ๋ก์ ํธ์ HealthKit ์ถ๊ฐํ๊ธฐ
Step 1: Capability ์ถ๊ฐXcode์์ ํ๋ก์ ํธ๋ฅผ ์ด๊ณ , ํ๊ฒ์ ์ ํํ ๋ค์ 'Signing & Capabilities' ํญ์ผ๋ก ๊ฐ. ๊ฑฐ๊ธฐ์ '+' ๋ฒํผ์ ๋๋ฌ์ 'HealthKit'์ ์ถ๊ฐํ๋ฉด ๋ผ. ์ด๋ ๊ฒ ํ๋ฉด ์๋์ผ๋ก entitlements ํ์ผ์ด ์์ฑ๋๊ณ ํ์ํ ์ค์ ์ด ๋ค์ด๊ฐ. โ
Step 2: Info.plist ์ค์
์ฌ์ฉ์์๊ฒ ๊ถํ์ ์์ฒญํ ๋ ์ ์ด ๋ฐ์ดํฐ๊ฐ ํ์ํ์ง ์ค๋ช ํด์ผ ํด. Info.plist์ ๋ค์ ํค๋ค์ ์ถ๊ฐํ์:
<key>NSHealthShareUsageDescription</key>
<string>๊ฑด๊ฐ ๋ฐ์ดํฐ๋ฅผ ์ฝ์ด์ ๋ง์ถคํ ์ด๋ ๊ณํ์ ์ ๊ณตํฉ๋๋ค.</string>
<key>NSHealthUpdateUsageDescription</key>
<string>์ด๋ ๊ธฐ๋ก์ ๊ฑด๊ฐ ์ฑ์ ์ ์ฅํฉ๋๋ค.</string>์ด ์ค๋ช ์ ์ฌ์ฉ์๊ฐ ์ค์ ๋ก ๋ณด๊ฒ ๋๋๊น ์น์ ํ๊ณ ๋ช ํํ๊ฒ ์์ฑํ๋ ๊ฒ ์ค์ํด! ๐
HKHealthStore ์ด๊ธฐํ
HealthKit์ ๋ชจ๋ ์์ ์ HKHealthStore๋ฅผ ํตํด ์ด๋ฃจ์ด์ ธ. ์ด๊ฒ ๋ฐ๋ก ๊ฑด๊ฐ ๋ฐ์ดํฐ ์ ์ฅ์์ ์ํตํ๋ ์ฐฝ๊ตฌ์ผ.import HealthKit
class HealthKitManager {
let healthStore = HKHealthStore()
// HealthKit ์ฌ์ฉ ๊ฐ๋ฅ ์ฌ๋ถ ํ์ธ
func isHealthKitAvailable() -> Bool {
return HKHealthStore.isHealthDataAvailable()
}
}
โ ๏ธ ์ฃผ์์ฌํญ
HealthKit์ iPad์์๋ ์ฌ์ฉํ ์ ์์ด! ์์ดํฐ๊ณผ ์ ํ์์น์์๋ง ์๋ํ๋๊น, ์ฑ์ ๋ง๋ค ๋ ์ด ์ ์ ๊ผญ ๊ณ ๋ คํด์ผ ํด. isHealthDataAvailable() ๋ฉ์๋๋ก ํญ์ ์ฒดํฌํ๋ ์ต๊ด์ ๋ค์ด์.
HealthKit์ iPad์์๋ ์ฌ์ฉํ ์ ์์ด! ์์ดํฐ๊ณผ ์ ํ์์น์์๋ง ์๋ํ๋๊น, ์ฑ์ ๋ง๋ค ๋ ์ด ์ ์ ๊ผญ ๊ณ ๋ คํด์ผ ํด. isHealthDataAvailable() ๋ฉ์๋๋ก ํญ์ ์ฒดํฌํ๋ ์ต๊ด์ ๋ค์ด์.
๊ถํ ์์ฒญํ๊ธฐ
์ด์ ์ฌ์ฉ์์๊ฒ ๊ฑด๊ฐ ๋ฐ์ดํฐ ์ ๊ทผ ๊ถํ์ ์์ฒญํด๋ณด์. ๋จผ์ ์ด๋ค ๋ฐ์ดํฐ ํ์ ์ ์ ๊ทผํ ๊ฑด์ง ์ ์ํด์ผ ํด.func requestAuthorization(completion: @escaping (Bool, Error?) -> Void) {
// ์ฝ๊ณ ์ถ์ ๋ฐ์ดํฐ ํ์
๋ค
let readTypes: Set<HKObjectType> = [
HKObjectType.quantityType(forIdentifier: .stepCount)!,
HKObjectType.quantityType(forIdentifier: .heartRate)!,
HKObjectType.quantityType(forIdentifier: .activeEnergyBurned)!,
HKObjectType.categoryType(forIdentifier: .sleepAnalysis)!
]
// ์ฐ๊ณ ์ถ์ ๋ฐ์ดํฐ ํ์
๋ค
let writeTypes: Set<HKSampleType> = [
HKObjectType.quantityType(forIdentifier: .stepCount)!,
HKObjectType.workoutType()
]
// ๊ถํ ์์ฒญ
healthStore.requestAuthorization(toShare: writeTypes, read: readTypes) { success, error in
completion(success, error)
}
}์ด ์ฝ๋๋ฅผ ์คํํ๋ฉด ์ฌ์ฉ์์๊ฒ ๊ถํ ์์ฒญ ํ์ ์ด ๋จ๊ฒ ๋ผ. ๊ฐ ๋ฐ์ดํฐ ํ์ ๋ง๋ค ๊ฐ๋ณ์ ์ผ๋ก ํ์ฉ/๊ฑฐ๋ถ๋ฅผ ์ ํํ ์ ์์ด. ๐๏ธ
๐ก ๊ถํ ์์ฒญ ํ
๊ถํ์ ์์ฒญํ๋ ํ์ด๋ฐ์ด ์ ๋ง ์ค์ํด! ์ฑ์ ์ฒ์ ์คํํ์๋ง์ ์์ฒญํ๋ฉด ์ฌ์ฉ์๊ฐ ๊ฑฐ๋ถํ ํ๋ฅ ์ด ๋์. ๋์ ํด๋น ๊ธฐ๋ฅ์ ์ฌ์ฉํ๋ ค๊ณ ํ ๋, ์ ํ์ํ์ง ์ค๋ช ํ๊ณ ๋์ ์์ฒญํ๋ ๊ฒ ํจ์ฌ ํจ๊ณผ์ ์ด์ผ. ๐ฏ
๊ถํ์ ์์ฒญํ๋ ํ์ด๋ฐ์ด ์ ๋ง ์ค์ํด! ์ฑ์ ์ฒ์ ์คํํ์๋ง์ ์์ฒญํ๋ฉด ์ฌ์ฉ์๊ฐ ๊ฑฐ๋ถํ ํ๋ฅ ์ด ๋์. ๋์ ํด๋น ๊ธฐ๋ฅ์ ์ฌ์ฉํ๋ ค๊ณ ํ ๋, ์ ํ์ํ์ง ์ค๋ช ํ๊ณ ๋์ ์์ฒญํ๋ ๊ฒ ํจ์ฌ ํจ๊ณผ์ ์ด์ผ. ๐ฏ
๐ HealthKit ๋ฐ์ดํฐ ์ฝ๊ธฐ
๊ฑธ์ ์ ๊ฐ์ ธ์ค๊ธฐ
๊ฐ์ฅ ๊ธฐ๋ณธ์ ์ธ ์์ ๋ก ์ค๋ ๊ฑธ์ ๊ฑธ์ ์๋ฅผ ๊ฐ์ ธ์๋ณด์. ์ด๊ฑด ๊ฑฐ์ ๋ชจ๋ ํผํธ๋์ค ์ฑ์์ ์ฌ์ฉํ๋ ๊ธฐ๋ฅ์ด์ผ! ๐func getTodaySteps(completion: @escaping (Double?, Error?) -> Void) {
guard let stepType = HKQuantityType.quantityType(forIdentifier: .stepCount) else {
completion(nil, nil)
return
}
// ์ค๋ ์์ ๋ถํฐ ์ง๊ธ๊น์ง
let now = Date()
let startOfDay = Calendar.current.startOfDay(for: now)
let predicate = HKQuery.predicateForSamples(withStart: startOfDay, end: now, options: .strictStartDate)
// ํต๊ณ ์ฟผ๋ฆฌ ์์ฑ
let query = HKStatisticsQuery(quantityType: stepType, quantitySamplePredicate: predicate, options: .cumulativeSum) { _, result, error in
guard let result = result, let sum = result.sumQuantity() else {
completion(nil, error)
return
}
let steps = sum.doubleValue(for: HKUnit.count())
completion(steps, nil)
}
healthStore.execute(query)
}์ด ์ฝ๋๋ HKStatisticsQuery๋ฅผ ์ฌ์ฉํด์ ์ค๋ ํ๋ฃจ ๋์์ ๊ฑธ์ ์ ํฉ๊ณ๋ฅผ ๊ตฌํด. ๊ฒฐ๊ณผ๋ Double ํ์ ์ผ๋ก ๋ฐํ๋์ง. ๐
์ฌ๋ฐ์ ๋ฐ์ดํฐ ๊ฐ์ ธ์ค๊ธฐ
์ฌ๋ฐ์๋ ์ข ๋ ๋ณต์กํด. ์๋ํ๋ฉด ์๊ฐ์ ๋ฐ๋ผ ๊ณ์ ๋ณํ๋ ๊ฐ์ด๊ฑฐ๋ . ์ต๊ทผ 10๊ฐ์ ์ฌ๋ฐ์ ์ธก์ ๊ฐ์ ๊ฐ์ ธ์๋ณด์. โค๏ธfunc getRecentHeartRates(completion: @escaping ([HKQuantitySample]?, Error?) -> Void) {
guard let heartRateType = HKQuantityType.quantityType(forIdentifier: .heartRate) else {
completion(nil, nil)
return
}
// ์ต์ ๋ฐ์ดํฐ๋ถํฐ ์ ๋ ฌ
let sortDescriptor = NSSortDescriptor(key: HKSampleSortIdentifierStartDate, ascending: false)
// ์ํ ์ฟผ๋ฆฌ ์์ฑ (์ต๋ 10๊ฐ)
let query = HKSampleQuery(sampleType: heartRateType, predicate: nil, limit: 10, sortDescriptors: [sortDescriptor]) { _, samples, error in
guard let samples = samples as? [HKQuantitySample] else {
completion(nil, error)
return
}
completion(samples, nil)
}
healthStore.execute(query)
}์ด๋ ๊ฒ ๊ฐ์ ธ์จ ์ํ๋ค์ ๊ฐ๊ฐ ์ธก์ ์๊ฐ๊ณผ ๊ฐ์ ๊ฐ์ง๊ณ ์์ด. ์ด๊ฑธ ํ์ฉํด์ ์ฌ๋ฐ์ ๊ทธ๋ํ๋ฅผ ๊ทธ๋ฆด ์๋ ์์ง! ๐
์๋ฉด ๋ฐ์ดํฐ ๋ถ์ํ๊ธฐ
์๋ฉด ๋ฐ์ดํฐ๋ ์นดํ ๊ณ ๋ฆฌ ํ์ ์ด๋ผ ์กฐ๊ธ ๋ค๋ฅด๊ฒ ๋ค๋ค์ผ ํด. ์ง๋ 7์ผ๊ฐ์ ์๋ฉด ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์๋ณด์. ๐ดfunc getWeeklySleepData(completion: @escaping ([HKCategorySample]?, Error?) -> Void) {
guard let sleepType = HKCategoryType.categoryType(forIdentifier: .sleepAnalysis) else {
completion(nil, nil)
return
}
// 7์ผ ์ ๋ถํฐ ์ง๊ธ๊น์ง
let endDate = Date()
let startDate = Calendar.current.date(byAdding: .day, value: -7, to: endDate)!
let predicate = HKQuery.predicateForSamples(withStart: startDate, end: endDate, options: .strictStartDate)
let sortDescriptor = NSSortDescriptor(key: HKSampleSortIdentifierStartDate, ascending: false)
let query = HKSampleQuery(sampleType: sleepType, predicate: predicate, limit: HKObjectQueryNoLimit, sortDescriptors: [sortDescriptor]) { _, samples, error in
guard let samples = samples as? [HKCategorySample] else {
completion(nil, error)
return
}
completion(samples, nil)
}
healthStore.execute(query)
}์๋ฉด ๋ฐ์ดํฐ๋ HKCategoryValueSleepAnalysis enum์ผ๋ก ๊ตฌ๋ถ๋ผ:
โข .inBed - ์นจ๋์ ๋์์์
โข .asleepUnspecified - ์ ๋ ์ํ (๋จ๊ณ ๋ฏธ๋ถ๋ฅ)
โข .awake - ๊นจ์ด์์
โข .asleepCore - ์์ ์๋ฉด
โข .asleepDeep - ๊น์ ์๋ฉด
โข .asleepREM - REM ์๋ฉด
โ๏ธ HealthKit ๋ฐ์ดํฐ ์ฐ๊ธฐ
์ด๋ ์ธ์ ๊ธฐ๋กํ๊ธฐ
์ด์ ๋ฐ์ดํฐ๋ฅผ HealthKit์ ์ ์ฅํด๋ณด์. ์ฌ์ฉ์๊ฐ 30๋ถ ๋์ ๋ฌ๋ฆฌ๊ธฐ๋ฅผ ํ๋ค๊ณ ๊ฐ์ ํ๊ณ , ์ด ์ด๋ ์ธ์ ์ ๊ธฐ๋กํด๋ณผ๊ฒ. ๐โโ๏ธfunc saveWorkout(completion: @escaping (Bool, Error?) -> Void) {
// ์ด๋ ์์/์ข
๋ฃ ์๊ฐ
let endDate = Date()
let startDate = endDate.addingTimeInterval(-30 * 60) // 30๋ถ ์
// ์๋ชจํ ์นผ๋ก๋ฆฌ (์: 250kcal)
let energyBurned = HKQuantity(unit: HKUnit.kilocalorie(), doubleValue: 250)
// ์ด๋ ๊ฑฐ๋ฆฌ (์: 5km)
let distance = HKQuantity(unit: HKUnit.meter(), doubleValue: 5000)
// ์ด๋ ๊ฐ์ฒด ์์ฑ
let workout = HKWorkout(activityType: .running,
start: startDate,
end: endDate,
duration: 30 * 60,
totalEnergyBurned: energyBurned,
totalDistance: distance,
metadata: nil)
// HealthKit์ ์ ์ฅ
healthStore.save(workout) { success, error in
completion(success, error)
}
}HKWorkout์ ์ด๋ ์ธ์ ์ ๋ํ๋ด๋ ํน๋ณํ ๊ฐ์ฒด์ผ. ์ด๋ ์ข ๋ฅ(activityType)๋ ๋ฌ๋ฆฌ๊ธฐ, ์์ ๊ฑฐ, ์์, ์๊ฐ ๋ฑ ์ ๋ง ๋ค์ํ๊ฒ ์ง์๋ผ! ๐ดโโ๏ธ๐โโ๏ธ๐งโโ๏ธ
์ฌ๋ฐ์ ๋ฐ์ดํฐ ์ ์ฅํ๊ธฐ
์ฌ๋ฐ์ ๊ฐ์ ์์น ๋ฐ์ดํฐ๋ฅผ ์ ์ฅํ ๋๋ HKQuantitySample์ ์ฌ์ฉํด.func saveHeartRate(value: Double, date: Date, completion: @escaping (Bool, Error?) -> Void) {
guard let heartRateType = HKQuantityType.quantityType(forIdentifier: .heartRate) else {
completion(false, nil)
return
}
// ์ฌ๋ฐ์ ๋จ์๋ ๋ถ๋น ํ์ (bpm)
let heartRateQuantity = HKQuantity(unit: HKUnit.count().unitDivided(by: HKUnit.minute()), doubleValue: value)
// ์ํ ์์ฑ
let heartRateSample = HKQuantitySample(type: heartRateType,
quantity: heartRateQuantity,
start: date,
end: date)
// ์ ์ฅ
healthStore.save(heartRateSample) { success, error in
completion(success, error)
}
}์ด๋ ๊ฒ ์ ์ฅํ ๋ฐ์ดํฐ๋ ๊ฑด๊ฐ ์ฑ์์๋ ๋ฐ๋ก ํ์ธํ ์ ์์ด! ๐ฑ
๋ฐฐ์น๋ก ์ฌ๋ฌ ๋ฐ์ดํฐ ์ ์ฅํ๊ธฐ
์ฌ๋ฌ ๊ฐ์ ๋ฐ์ดํฐ๋ฅผ ํ ๋ฒ์ ์ ์ฅํ๊ณ ์ถ์ ๋๋ ๋ฐฐ์น ์ ์ฅ์ ์ฌ์ฉํ๋ฉด ํจ์จ์ ์ด์ผ.func saveBatchSamples(samples: [HKSample], completion: @escaping (Bool, Error?) -> Void) {
healthStore.save(samples) { success, error in
completion(success, error)
}
}์๋ฅผ ๋ค์ด ์ด๋ ์ค์ ์ธก์ ํ ์ฌ๋ฌ ์ฌ๋ฐ์ ๋ฐ์ดํฐ๋ฅผ ๋ชจ์๋๋ค๊ฐ ํ ๋ฒ์ ์ ์ฅํ ์ ์์ง. ์ด๋ ๊ฒ ํ๋ฉด ๋ฐฐํฐ๋ฆฌ๋ ์ ์ฝ๋๊ณ ์ฑ๋ฅ๋ ์ข์์ ธ! ๐
๐ ์ค์๊ฐ ๋ฐ์ดํฐ ๋ชจ๋ํฐ๋ง
Observer Query๋ก ๋ณํ ๊ฐ์งํ๊ธฐ
HealthKit์ ์ ๋ง ๊ฐ๋ ฅํ ๊ธฐ๋ฅ ์ค ํ๋๊ฐ ๋ฐ๋ก ์ค์๊ฐ ๋ฐ์ดํฐ ๋ชจ๋ํฐ๋ง์ด์ผ. HKObserverQuery๋ฅผ ์ฌ์ฉํ๋ฉด ํน์ ๋ฐ์ดํฐ๊ฐ ์ ๋ฐ์ดํธ๋ ๋๋ง๋ค ์๋ฆผ์ ๋ฐ์ ์ ์์ด! ๐func setupStepCountObserver() {
guard let stepType = HKQuantityType.quantityType(forIdentifier: .stepCount) else {
return
}
let query = HKObserverQuery(sampleType: stepType, predicate: nil) { query, completionHandler, error in
if error != nil {
print("Observer query failed: \(error!.localizedDescription)")
return
}
// ๊ฑธ์ ์๊ฐ ์
๋ฐ์ดํธ๋์์ ๋ ์คํ๋ ์ฝ๋
self.getTodaySteps { steps, error in
if let steps = steps {
print("์๋ก์ด ๊ฑธ์ ์: \(steps)")
// UI ์
๋ฐ์ดํธ ๋ฑ
}
}
// ๋ฐ๋์ completion handler๋ฅผ ํธ์ถํด์ผ ํจ!
completionHandler()
}
healthStore.execute(query)
// ๋ฐฑ๊ทธ๋ผ์ด๋์์๋ ๋์ํ๋๋ก ์ค์
healthStore.enableBackgroundDelivery(for: stepType, frequency: .immediate) { success, error in
if success {
print("๋ฐฑ๊ทธ๋ผ์ด๋ ์
๋ฐ์ดํธ ํ์ฑํ๋จ")
}
}
}์ด ์ฝ๋๋ ๊ฑธ์ ์๊ฐ ๋ณ๊ฒฝ๋ ๋๋ง๋ค ์๋์ผ๋ก ์๋ฆผ์ ๋ฐ์. ์ฌ์ง์ด ์ฑ์ด ๋ฐฑ๊ทธ๋ผ์ด๋์ ์๊ฑฐ๋ ์์ ํ ์ข ๋ฃ๋ ์ํ์์๋ ์๋ํด! ๐ฎ
Anchored Object Query๋ก ์ฆ๋ถ ์ ๋ฐ์ดํธ
HKAnchoredObjectQuery๋ ๋ง์ง๋ง์ผ๋ก ๊ฐ์ ธ์จ ์ดํ์ ์๋ก์ด ๋ฐ์ดํฐ๋ง ๊ฐ์ ธ์ค๋ ๋๋ํ ์ฟผ๋ฆฌ์ผ. ์ ์ฒด ๋ฐ์ดํฐ๋ฅผ ๋งค๋ฒ ๋ค์ ๊ฐ์ ธ์ฌ ํ์๊ฐ ์์ผ๋๊น ํจ์ฌ ํจ์จ์ ์ด์ง! ๐var anchor: HKQueryAnchor?
func fetchNewHeartRateData() {
guard let heartRateType = HKQuantityType.quantityType(forIdentifier: .heartRate) else {
return
}
let query = HKAnchoredObjectQuery(type: heartRateType,
predicate: nil,
anchor: anchor,
limit: HKObjectQueryNoLimit) { query, newSamples, deletedSamples, newAnchor, error in
// ์๋ก์ด ์ต์ปค ์ ์ฅ (๋ค์ ์ฟผ๋ฆฌ์์ ์ฌ์ฉ)
self.anchor = newAnchor
// ์๋ก ์ถ๊ฐ๋ ์ํ๋ค ์ฒ๋ฆฌ
if let samples = newSamples as? [HKQuantitySample] {
for sample in samples {
let heartRate = sample.quantity.doubleValue(for: HKUnit.count().unitDivided(by: .minute()))
print("์๋ก์ด ์ฌ๋ฐ์: \(heartRate) bpm")
}
}
// ์ญ์ ๋ ์ํ๋ค ์ฒ๋ฆฌ
if let deleted = deletedSamples {
print("\(deleted.count)๊ฐ์ ์ํ์ด ์ญ์ ๋จ")
}
}
// ์
๋ฐ์ดํธ ํธ๋ค๋ฌ ์ค์ (๋ฐ์ดํฐ ๋ณ๊ฒฝ ์ ์๋ ํธ์ถ)
query.updateHandler = { query, newSamples, deletedSamples, newAnchor, error in
self.anchor = newAnchor
// ์๋ก์ด ๋ฐ์ดํฐ ์ฒ๋ฆฌ...
}
healthStore.execute(query)
}์ต์ปค๋ ์ผ์ข ์ ๋ถ๋งํฌ ๊ฐ์ ๊ฑฐ์ผ. "์ฌ๊ธฐ๊น์ง ์ฝ์์ด์"๋ผ๊ณ ํ์ํด๋๋ ๊ฑฐ์ง. ๋ค์์ ์ฟผ๋ฆฌํ ๋๋ ๊ทธ ์ง์ ๋ถํฐ ์๋ก์ด ๋ฐ์ดํฐ๋ง ๊ฐ์ ธ์ค๋ฉด ๋ผ! ๐
๐ฅ CareKit ์๊ฐ
์, ์ด์ CareKit์ผ๋ก ๋์ด๊ฐ๋ณด์! HealthKit์ด ๊ฑด๊ฐ ๋ฐ์ดํฐ๋ฅผ ๋ค๋ฃจ๋ ๋๊ตฌ๋ผ๋ฉด, CareKit์ ๊ทธ ๋ฐ์ดํฐ๋ฅผ ํ์ฉํด์ ํ์ ์ผ์ด์ ์น๋ฃ ๊ณํ์ ๊ด๋ฆฌํ๋ ํ๋ ์์ํฌ์ผ. ๐
๐ฏ CareKit์ ์ฃผ์ ๋ชฉ์
1. ์น๋ฃ ๊ณํ ๊ด๋ฆฌ - ์ฝ ๋ณต์ฉ, ์ด๋, ์๋จ ๋ฑ์ ์ผ์ ์ ์ฒด๊ณ์ ์ผ๋ก ๊ด๋ฆฌ
2. ์ฆ์ ์ถ์ - ํต์ฆ, ๊ธฐ๋ถ, ์ฆ์์ ๋ณํ๋ฅผ ๊ธฐ๋กํ๊ณ ๋ถ์
3. ์ธ์ฌ์ดํธ ์ ๊ณต - ์์ง๋ ๋ฐ์ดํฐ๋ฅผ ์๊ฐํํ๊ณ ํจํด์ ๋ฐ๊ฒฌ
4. ์๋ฃ์ง๊ณผ ์ํต - ํ์ ๋ฐ์ดํฐ๋ฅผ ์๋ฃ์ง๊ณผ ์์ ํ๊ฒ ๊ณต์
CareKit์ 2016๋
์ ์ฒ์ ๊ณต๊ฐ๋์๊ณ , 2019๋
์ ์์ ํ ์๋กญ๊ฒ ์ฌ์ค๊ณ๋ CareKit 2.0์ด ๋์์ด. ์ง๊ธ ์ฐ๋ฆฌ๊ฐ ๋ฐฐ์ธ ๊ฑด ์ด ์ต์ ๋ฒ์ ์ด์ผ! โจ
1. ์น๋ฃ ๊ณํ ๊ด๋ฆฌ - ์ฝ ๋ณต์ฉ, ์ด๋, ์๋จ ๋ฑ์ ์ผ์ ์ ์ฒด๊ณ์ ์ผ๋ก ๊ด๋ฆฌ
2. ์ฆ์ ์ถ์ - ํต์ฆ, ๊ธฐ๋ถ, ์ฆ์์ ๋ณํ๋ฅผ ๊ธฐ๋กํ๊ณ ๋ถ์
3. ์ธ์ฌ์ดํธ ์ ๊ณต - ์์ง๋ ๋ฐ์ดํฐ๋ฅผ ์๊ฐํํ๊ณ ํจํด์ ๋ฐ๊ฒฌ
4. ์๋ฃ์ง๊ณผ ์ํต - ํ์ ๋ฐ์ดํฐ๋ฅผ ์๋ฃ์ง๊ณผ ์์ ํ๊ฒ ๊ณต์
CareKit์ 4๊ฐ์ง ํต์ฌ ๋ชจ๋
๐ Care (์ผ์ด)ํ์์ ์น๋ฃ ๊ณํ์ ๊ด๋ฆฌํ๋ ๋ชจ๋์ด์ผ. ์ฝ ๋ณต์ฉ ์๊ฐ, ์ด๋ ์ผ์ , ๋ณ์ ๋ฐฉ๋ฌธ ๋ฑ์ ์ค์ผ์ค๋งํ๊ณ ์ถ์ ํ ์ ์์ด.
๐ Insights (์ธ์ฌ์ดํธ)
์์ง๋ ๋ฐ์ดํฐ๋ฅผ ๋ถ์ํ๊ณ ์๊ฐํํด์ ์๋ฏธ ์๋ ์ ๋ณด๋ฅผ ์ ๊ณตํด. ์๋ฅผ ๋ค์ด "์ด๋์ ํ ๋ ์ ์๋ฉด์ ์ง์ด 20% ํฅ์๋์์ต๋๋ค" ๊ฐ์ ์ธ์ฌ์ดํธ๋ฅผ ๋ณด์ฌ์ค ์ ์์ง.
๐ Connect (์ฐ๊ฒฐ)
ํ์์ ์๋ฃ์ง, ๊ฐ์กฑ์ ์ฐ๊ฒฐํด์ฃผ๋ ๋ชจ๋์ด์ผ. ๋ฐ์ดํฐ๋ฅผ ์์ ํ๊ฒ ๊ณต์ ํ๊ณ ๋ฉ์์ง๋ฅผ ์ฃผ๊ณ ๋ฐ์ ์ ์์ด.
๐จ UI (์ฌ์ฉ์ ์ธํฐํ์ด์ค)
๋ฏธ๋ฆฌ ๋ง๋ค์ด์ง ์๋ฆ๋ค์ด UI ์ปดํฌ๋ํธ๋ค์ ์ ๊ณตํด. ์ง์ ๋์์ธํ ํ์ ์์ด ๋ฐ๋ก ์ฌ์ฉํ ์ ์์ด์ ๊ฐ๋ฐ ์๊ฐ์ด ์์ฒญ ๋จ์ถ๋ผ! ๐
๐ป CareKit ์์ํ๊ธฐ
ํ๋ก์ ํธ ์ค์
CareKit์ ์ฌ์ฉํ๋ ค๋ฉด ๋จผ์ ํ๋ก์ ํธ์ ์ถ๊ฐํด์ผ ํด. Swift Package Manager๋ฅผ ์ฌ์ฉํ๋ฉด ์ ๋ง ๊ฐ๋จํด!Step 1: Swift Package ์ถ๊ฐ
Xcode์์ File โ Add Packages... ๋ฉ๋ด๋ฅผ ์ ํํ๊ณ , ๋ค์ URL์ ์ ๋ ฅํด:
https://github.com/carekit-apple/CareKit
Step 2: ํ์ํ ๋ชจ๋ ์ ํ
CareKit์ ์ฌ๋ฌ ๋ชจ๋๋ก ๋๋์ด ์์ด. ํ์ํ ๊ฒ๋ง ์ ํํด์ ์ถ๊ฐํ๋ฉด ๋ผ:
โข CareKit - ์ ์ฒด ํ๋ ์์ํฌ
โข CareKitUI - UI ์ปดํฌ๋ํธ๋ง
โข CareKitStore - ๋ฐ์ดํฐ ์ ์ฅ์๋ง
๋ณดํต์ CareKit ์ ์ฒด๋ฅผ ์ถ๊ฐํ๋ ๊ฒ ํธํด! ๐ฆ
CareStore ์ด๊ธฐํ
CareKit์ ๋ชจ๋ ๋ฐ์ดํฐ๋ OCKStore์ ์ ์ฅ๋ผ. ์ด๊ฑด Core Data ๊ธฐ๋ฐ์ ๋ก์ปฌ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ผ.import CareKit
import CareKitStore
class CareKitManager {
// ์ฑ ๊ทธ๋ฃน์ ์ฌ์ฉํ๋ฉด ์ฌ๋ฌ ์ฑ/ํ์ฅ ํ๋ก๊ทธ๋จ ๊ฐ ๋ฐ์ดํฐ ๊ณต์ ๊ฐ๋ฅ
let storeManager: OCKSynchronizedStoreManager = {
let store = OCKStore(name: "myHealthApp", type: .onDisk)
let manager = OCKSynchronizedStoreManager(wrapping: store)
return manager
}()
var store: OCKStore {
return storeManager.store
}
}OCKStore๋ ์๋์ผ๋ก ๋ฐ์ดํฐ๋ฅผ ๋์คํฌ์ ์ ์ฅํ๊ณ , iCloud ๋๊ธฐํ๋ ์ง์ํด. ์ ๋ง ํธ๋ฆฌํ์ง? โ๏ธ
๐ ์น๋ฃ ๊ณํ ๋ง๋ค๊ธฐ
Task ์์ฑํ๊ธฐ
CareKit์์ ๊ฐ์ฅ ๊ธฐ๋ณธ์ด ๋๋ ๊ฐ๋ ์ด Task์ผ. ์ฝ ๋ณต์ฉ, ์ด๋, ์ธก์ ๋ฑ ํ์๊ฐ ํด์ผ ํ ๋ชจ๋ ํ๋์ Task๋ก ํํํด.func createMedicationTask() async throws {
// ์ฝ ๋ณต์ฉ ์ค์ผ์ค ์์ฑ (๋งค์ผ ์์นจ 8์, ์ ๋
8์)
let schedule = OCKSchedule.dailyAtTimes(
hour: [8, 20],
minutes: [0, 0],
start: Date(),
end: nil,
text: "์์นจ๊ณผ ์ ๋
์ํ"
)
// Task ์์ฑ
var task = OCKTask(
id: "medication-aspirin",
title: "์์คํผ๋ฆฐ ๋ณต์ฉ",
carePlanUUID: nil,
schedule: schedule
)
task.instructions = "๋ฌผ๊ณผ ํจ๊ป 1์ ์ ๋ณต์ฉํ์ธ์."
task.impactsAdherence = true // ์ค์์จ ๊ณ์ฐ์ ํฌํจ
// ์ ์ฅ
try await store.addTask(task)
print("์ฝ ๋ณต์ฉ Task๊ฐ ์์ฑ๋์์ต๋๋ค!")
}์ด๋ ๊ฒ ๋ง๋ Task๋ ์๋์ผ๋ก ๋งค์ผ ์์นจ 8์์ ์ ๋ 8์์ ์๋ฆผ์ ๋ณด๋ด์ค. ์ฌ์ฉ์๊ฐ ์๋ฃ ์ฒดํฌ๋ฅผ ํ๋ฉด ๊ทธ ๊ธฐ๋ก๋ ์๋์ผ๋ก ์ ์ฅ๋๊ณ ! โ
๋ค์ํ ์ค์ผ์ค ํจํด
CareKit์ ์ ๋ง ์ ์ฐํ ์ค์ผ์ค๋ง์ ์ง์ํด. ๋ช ๊ฐ์ง ์์ ๋ฅผ ๋ณด์:// 1. ๋งค์ผ ํน์ ์๊ฐ์
let dailySchedule = OCKSchedule.dailyAtTime(
hour: 9,
minutes: 0,
start: Date(),
end: nil,
text: "๋งค์ผ ์์นจ 9์"
)
// 2. ์ฃผ 3ํ (์, ์, ๊ธ)
let weeklySchedule = OCKSchedule.weeklyAtTime(
weekday: [.monday, .wednesday, .friday],
hours: 14,
minutes: 0,
start: Date(),
end: nil
)
// 3. 4์๊ฐ๋ง๋ค
let intervalSchedule = OCKSchedule(
composing: [
OCKScheduleElement(
start: Date(),
end: nil,
interval: DateComponents(hour: 4),
text: "4์๊ฐ๋ง๋ค"
)
]
)
// 4. ํ์ํ ๋๋ง (ํ์ ์ ํ ์์)
let asNeededSchedule = OCKSchedule(
composing: [
OCKScheduleElement(
start: Date(),
end: nil,
interval: DateComponents(day: 1),
text: "ํ์์",
targetValues: [],
duration: .allDay
)
]
)์ด๋ฐ ์์ผ๋ก ๊ฑฐ์ ๋ชจ๋ ์ข ๋ฅ์ ์ค์ผ์ค์ ํํํ ์ ์์ด! ๐๏ธ
Outcome ๊ธฐ๋กํ๊ธฐ
์ฌ์ฉ์๊ฐ Task๋ฅผ ์๋ฃํ๋ฉด Outcome์ ์์ฑํด์ ๊ธฐ๋กํด์ผ ํด.func completeTask(taskID: String, date: Date) async throws {
// ํด๋น ๋ ์ง์ Task ์ด๋ฒคํธ ๊ฐ์ ธ์ค๊ธฐ
let query = OCKEventQuery(for: date)
let events = try await store.fetchEvents(taskID: taskID, query: query)
guard let event = events.first else {
print("์ด๋ฒคํธ๋ฅผ ์ฐพ์ ์ ์์ต๋๋ค")
return
}
// Outcome ์์ฑ
let outcome = OCKOutcome(
taskUUID: event.task.uuid,
taskOccurrenceIndex: event.scheduleEvent.occurrence,
values: [OCKOutcomeValue(true)]
)
// ์ ์ฅ
try await store.addOutcome(outcome)
print("Task ์๋ฃ!")
}Outcome์๋ ๋จ์ํ "์๋ฃํ๋ค/์ํ๋ค"๋ฟ๋ง ์๋๋ผ ์ถ๊ฐ ๋ฐ์ดํฐ๋ ์ ์ฅํ ์ ์์ด. ์๋ฅผ ๋ค์ด ํ์์ ์ธก์ ํ๋ค๋ฉด ๊ทธ ์์น๋ฅผ ํจ๊ป ์ ์ฅํ๋ ๊ฑฐ์ง! ๐
๐จ CareKit UI ์ปดํฌ๋ํธ
CareKit์ ๊ฐ์ฅ ํฐ ์ฅ์ ์ค ํ๋๊ฐ ๋ฐ๋ก ๋ฏธ๋ฆฌ ๋ง๋ค์ด์ง ์๋ฆ๋ค์ด UI ์ปดํฌ๋ํธ๋ค์ด์ผ! ์ง์ ๋์์ธํ๊ณ ์ฝ๋ฉํ ํ์ ์์ด ๋ฐ๋ก ์ฌ์ฉํ ์ ์์ด์ ๊ฐ๋ฐ ์๋๊ฐ ์์ฒญ ๋นจ๋ผ์ ธ. ๐Task View Controller
๊ฐ์ฅ ๋ง์ด ์ฌ์ฉํ๋ ์ปดํฌ๋ํธ๊ฐ ๋ฐ๋ก OCKDailyTasksPageViewController์ผ. ํ๋ฃจ ๋์ ํด์ผ ํ Task๋ค์ ์์๊ฒ ๋ณด์ฌ์ฃผ์ง.import CareKit
import CareKitUI
import UIKit
class DailyTasksViewController: OCKDailyTasksPageViewController {
override func viewDidLoad() {
super.viewDidLoad()
// ์คํ ์ด ์ฐ๊ฒฐ
storeManager = careKitManager.storeManager
// ๋ค๋น๊ฒ์ด์
๋ฐ ์ค์
title = "์ค๋์ ํ ์ผ"
navigationController?.navigationBar.prefersLargeTitles = true
}
// Task ์นด๋ ์ปค์คํฐ๋ง์ด์ง
override func taskView(for task: OCKAnyTask, on date: Date) -> UIView? {
let card = OCKSimpleTaskViewController(task: task, eventQuery: .init(for: date), storeManager: storeManager)
// ์นด๋ ์คํ์ผ ์ปค์คํฐ๋ง์ด์ง
card.view.layer.cornerRadius = 12
card.view.layer.shadowColor = UIColor.black.cgColor
card.view.layer.shadowOpacity = 0.1
card.view.layer.shadowOffset = CGSize(width: 0, height: 2)
card.view.layer.shadowRadius = 4
return card.view
}
}์ด ์ฝ๋๋ง์ผ๋ก ์์ ํ ๊ธฐ๋ฅํ๋ Task ๊ด๋ฆฌ ํ๋ฉด์ด ๋ง๋ค์ด์ ธ! ์ฌ์ฉ์๋ ์ฒดํฌ๋ฐ์ค๋ฅผ ํญํด์ Task๋ฅผ ์๋ฃํ ์ ์๊ณ , ์๋์ผ๋ก ์งํ๋ฅ ์ด ์ ๋ฐ์ดํธ๋ผ. โจ
Chart View
๋ฐ์ดํฐ ์๊ฐํ๋ ์ ๋ง ์ฌ์. OCKCartesianChartViewController๋ฅผ ์ฌ์ฉํ๋ฉด ๋ฉ์ง ์ฐจํธ๋ฅผ ๋ฐ๋ก ๋ง๋ค ์ ์์ด! ๐func createHeartRateChart() -> UIViewController {
// ์ฐจํธ ๋ฐ์ดํฐ ์ค์
let dataSeriesConfig = OCKDataSeriesConfiguration(
taskID: "heart-rate",
legendTitle: "์ฌ๋ฐ์",
gradientStartColor: .systemRed,
gradientEndColor: .systemOrange,
markerSize: 8,
eventAggregator: .countOutcomeValues
)
// ์ฐจํธ ๋ทฐ ์ปจํธ๋กค๋ฌ ์์ฑ
let chartViewController = OCKCartesianChartViewController(
plotType: .line,
selectedDate: Date(),
configurations: [dataSeriesConfig],
storeManager: storeManager
)
chartViewController.chartView.headerView.titleLabel.text = "์ฌ๋ฐ์ ์ถ์ด"
chartViewController.chartView.headerView.detailLabel.text = "์ต๊ทผ 7์ผ"
return chartViewController
}์ด๋ ๊ฒ ํ๋ฉด ์๋์ผ๋ก ์ต๊ทผ 7์ผ๊ฐ์ ์ฌ๋ฐ์ ๋ฐ์ดํฐ๋ฅผ ์ ๊ทธ๋ํ๋ก ๋ณด์ฌ์ค. ์ฌ์ฉ์๊ฐ ๋ ์ง๋ฅผ ๋ณ๊ฒฝํ๋ฉด ์ฐจํธ๋ ์๋์ผ๋ก ์ ๋ฐ์ดํธ๋๊ณ ! ๐ฏ
Contact View
์๋ฃ์ง์ด๋ ๋ณดํธ์ ์ฐ๋ฝ์ฒ๋ฅผ ๋ณด์ฌ์ฃผ๋ ์นด๋๋ ์์ด.func createContactCard() -> UIView {
var contact = OCKContact(
id: "doctor-kim",
givenName: "๊น",
familyName: "์ฒ ์",
carePlanUUID: nil
)
contact.title = "๋ด๋น ์์ฌ"
contact.role = "๋ด๊ณผ ์ ๋ฌธ์"
contact.emailAddresses = [OCKLabeledValue(label: "์ด๋ฉ์ผ", value: "doctor.kim@hospital.com")]
contact.phoneNumbers = [OCKLabeledValue(label: "์ง๋ฃ์ค", value: "02-1234-5678")]
contact.messagingNumbers = [OCKLabeledValue(label: "์นด์นด์คํก", value: "doctorkim")]
contact.address = {
let address = OCKPostalAddress()
address.street = "์์ธ์ ๊ฐ๋จ๊ตฌ ํ
ํค๋๋ก 123"
address.city = "์์ธ"
address.state = "์์ธ"
address.postalCode = "06234"
return address
}()
let contactViewController = OCKDetailedContactViewController(contact: contact, storeManager: storeManager)
return contactViewController.view
}์ด ์นด๋์์ ๋ฐ๋ก ์ ํ๋ฅผ ๊ฑธ๊ฑฐ๋ ์ด๋ฉ์ผ์ ๋ณด๋ผ ์ ์์ด. ์ ๋ง ํธ๋ฆฌํ์ง? ๐
๐ HealthKit๊ณผ CareKit ์ฐ๋ํ๊ธฐ
์, ์ด์ ์ง์ง ๊ฐ๋ ฅํ ๋ถ๋ถ์ด์ผ! HealthKit๊ณผ CareKit์ ํจ๊ป ์ฌ์ฉํ๋ฉด ์๋์ง๊ฐ ์์ฒญ๋๊ฑฐ๋ . ๐ชHealthKit ๋ฐ์ดํฐ๋ฅผ CareKit Task๋ก ๋ณํ
์๋ฅผ ๋ค์ด ์ฌ์ฉ์๊ฐ ์ ํ์์น๋ก ์ด๋์ ํ๋ฉด, ๊ทธ ๋ฐ์ดํฐ๋ฅผ ์๋์ผ๋ก CareKit์ ์ด๋ Task ์๋ฃ๋ก ๊ธฐ๋กํ ์ ์์ด!class HealthCareIntegration {
let healthStore = HKHealthStore()
let careStore: OCKStore
init(careStore: OCKStore) {
self.careStore = careStore
}
// HealthKit ์ด๋ ๋ฐ์ดํฐ๋ฅผ CareKit Outcome์ผ๋ก ๋ณํ
func syncWorkoutData() async throws {
// ์ค๋์ ์ด๋ ๋ฐ์ดํฐ ๊ฐ์ ธ์ค๊ธฐ
let workouts = try await fetchTodayWorkouts()
// CareKit์ ์ด๋ Task ๊ฐ์ ธ์ค๊ธฐ
let query = OCKTaskQuery(for: Date())
let tasks = try await careStore.fetchTasks(query: query)
guard let exerciseTask = tasks.first(where: { $0.id == "daily-exercise" }) else {
return
}
// ์ด๋์ ํ๋ค๋ฉด Outcome ์์ฑ
if !workouts.isEmpty {
let totalDuration = workouts.reduce(0) { $0 + $1.duration }
let totalCalories = workouts.compactMap { $0.totalEnergyBurned?.doubleValue(for: .kilocalorie()) }.reduce(0, +)
let outcome = OCKOutcome(
taskUUID: exerciseTask.uuid,
taskOccurrenceIndex: 0,
values: [
OCKOutcomeValue(totalDuration, units: "๋ถ"),
OCKOutcomeValue(totalCalories, units: "kcal")
]
)
try await careStore.addOutcome(outcome)
print("์ด๋ ๋ฐ์ดํฐ ๋๊ธฐํ ์๋ฃ!")
}
}
private func fetchTodayWorkouts() async throws -> [HKWorkout] {
let workoutType = HKObjectType.workoutType()
let today = Calendar.current.startOfDay(for: Date())
let predicate = HKQuery.predicateForSamples(withStart: today, end: Date(), options: .strictStartDate)
return try await withCheckedThrowingContinuation { continuation in
let query = HKSampleQuery(sampleType: workoutType, predicate: predicate, limit: HKObjectQueryNoLimit, sortDescriptors: nil) { _, samples, error in
if let error = error {
continuation.resume(throwing: error)
} else {
continuation.resume(returning: samples as? [HKWorkout] ?? [])
}
}
healthStore.execute(query)
}
}
}์ด๋ ๊ฒ ํ๋ฉด ์ฌ์ฉ์๊ฐ ๋ณ๋๋ก ์ฑ์ ์ด์ด์ ์ฒดํฌํ์ง ์์๋, ์ ํ์์น๋ก ์ด๋๋ง ํ๋ฉด ์๋์ผ๋ก ๊ธฐ๋ก๋ผ! ์์ ์๋ํ์ง! ๐ค
CareKit ๋ฐ์ดํฐ๋ฅผ HealthKit์ ์ ์ฅ
๋ฐ๋๋ก CareKit์์ ๊ธฐ๋กํ ๋ฐ์ดํฐ๋ฅผ HealthKit์ ์ ์ฅํ ์๋ ์์ด. ์๋ฅผ ๋ค์ด ํ์ ์ธก์ ๊ฒฐ๊ณผ๋ฅผ HealthKit์๋ ์ ์ฅํ๋ฉด, ๊ฑด๊ฐ ์ฑ์์๋ ๋ณผ ์ ์๊ฒ ๋๋ ๊ฑฐ์ผ!func saveBloodPressureToHealthKit(systolic: Double, diastolic: Double, date: Date) async throws {
guard let systolicType = HKQuantityType.quantityType(forIdentifier: .bloodPressureSystolic),
let diastolicType = HKQuantityType.quantityType(forIdentifier: .bloodPressureDiastolic) else {
return
}
// ์์ถ๊ธฐ ํ์
let systolicQuantity = HKQuantity(unit: HKUnit.millimeterOfMercury(), doubleValue: systolic)
let systolicSample = HKQuantitySample(type: systolicType, quantity: systolicQuantity, start: date, end: date)
// ์ด์๊ธฐ ํ์
let diastolicQuantity = HKQuantity(unit: HKUnit.millimeterOfMercury(), doubleValue: diastolic)
let diastolicSample = HKQuantitySample(type: diastolicType, quantity: diastolicQuantity, start: date, end: date)
// ํ์์ ์๊ด๊ด๊ณ๊ฐ ์๋ ๋ฐ์ดํฐ์ด๋ฏ๋ก ํจ๊ป ์ ์ฅ
let correlation = HKCorrelation(type: HKCorrelationType.correlationType(forIdentifier: .bloodPressure)!,
start: date,
end: date,
objects: [systolicSample, diastolicSample])
try await healthStore.save(correlation)
print("ํ์ ๋ฐ์ดํฐ๊ฐ HealthKit์ ์ ์ฅ๋์์ต๋๋ค!")
}์ด๋ ๊ฒ ์๋ฐฉํฅ ๋๊ธฐํ๋ฅผ ๊ตฌํํ๋ฉด, ์ฌ์ฉ์๋ ์ด๋ ์ฑ์ ์ฌ์ฉํ๋ ์ผ๊ด๋ ๊ฑด๊ฐ ๋ฐ์ดํฐ๋ฅผ ๋ณผ ์ ์์ด! ๐
๐ ์ค์ ํ๋ก์ ํธ: ๋น๋จ๋ณ ๊ด๋ฆฌ ์ฑ
์ด๋ก ๋ง ๋ฐฐ์ฐ๋ฉด ์ฌ๋ฏธ์์ผ๋๊น, ์ค์ ๋ก ๊ฐ๋จํ ๋น๋จ๋ณ ๊ด๋ฆฌ ์ฑ์ ๋ง๋ค์ด๋ณด์! ์ด ์ฑ์ ๋ค์ ๊ธฐ๋ฅ๋ค์ ๊ฐ์ง ๊ฑฐ์ผ:
๐ฑ ์ฃผ์ ๊ธฐ๋ฅ
โข ํ๋น ์ธก์ ๊ธฐ๋ก ๋ฐ ์ถ์
โข ์ธ์๋ฆฐ ํฌ์ฌ ์ผ์ ๊ด๋ฆฌ
โข ์์ฌ ๋ฐ ํ์ํ๋ฌผ ์ญ์ทจ ๊ธฐ๋ก
โข ์ด๋ ํ๋ ์๋ ์ถ์
โข ํ๋น ์ถ์ด ๊ทธ๋ํ
โข ๋ด๋น ์์ฌ ์ฐ๋ฝ์ฒ
โข ํ๋น ์ธก์ ๊ธฐ๋ก ๋ฐ ์ถ์
โข ์ธ์๋ฆฐ ํฌ์ฌ ์ผ์ ๊ด๋ฆฌ
โข ์์ฌ ๋ฐ ํ์ํ๋ฌผ ์ญ์ทจ ๊ธฐ๋ก
โข ์ด๋ ํ๋ ์๋ ์ถ์
โข ํ๋น ์ถ์ด ๊ทธ๋ํ
โข ๋ด๋น ์์ฌ ์ฐ๋ฝ์ฒ
1๋จ๊ณ: ๋ฐ์ดํฐ ๋ชจ๋ธ ์ค๊ณ
๋จผ์ ์ด๋ค Task๋ค์ด ํ์ํ์ง ์ ์ํด๋ณด์.enum DiabetesTaskID {
static let bloodGlucose = "blood-glucose-measurement"
static let insulin = "insulin-injection"
static let breakfast = "meal-breakfast"
static let lunch = "meal-lunch"
static let dinner = "meal-dinner"
static let exercise = "daily-exercise"
}
class DiabetesCarePlan {
let store: OCKStore
init(store: OCKStore) {
self.store = store
}
func setupInitialTasks() async throws {
// 1. ํ๋น ์ธก์ Task (ํ๋ฃจ 4ํ: ์์นจ ๊ณต๋ณต, ์ํ 2์๊ฐ์ฉ)
let glucoseSchedule = OCKSchedule.dailyAtTimes(
hour: [7, 9, 13, 19],
minutes: [0, 0, 0, 0],
start: Date(),
end: nil,
text: "ํ๋น ์ธก์ ์๊ฐ"
)
var glucoseTask = OCKTask(id: DiabetesTaskID.bloodGlucose,
title: "ํ๋น ์ธก์ ",
carePlanUUID: nil,
schedule: glucoseSchedule)
glucoseTask.instructions = "ํ๋น ์ธก์ ๊ธฐ๋ก ํ๋น์ ์ธก์ ํ๊ณ ๊ฒฐ๊ณผ๋ฅผ ๊ธฐ๋กํ์ธ์."
glucoseTask.impactsAdherence = true
// 2. ์ธ์๋ฆฐ ํฌ์ฌ Task (์์นจ, ์ ๋
์์ )
let insulinSchedule = OCKSchedule.dailyAtTimes(
hour: [7, 18],
minutes: [30, 30],
start: Date(),
end: nil,
text: "์์ฌ 30๋ถ ์ "
)
var insulinTask = OCKTask(id: DiabetesTaskID.insulin,
title: "์ธ์๋ฆฐ ํฌ์ฌ",
carePlanUUID: nil,
schedule: insulinSchedule)
insulinTask.instructions = "์ฒ๋ฐฉ๋ ์ฉ๋์ ์ธ์๋ฆฐ์ ํฌ์ฌํ์ธ์."
insulinTask.impactsAdherence = true
// 3. ์์ฌ ๊ธฐ๋ก Tasks
let breakfastTask = createMealTask(id: DiabetesTaskID.breakfast, title: "์์นจ ์์ฌ", hour: 8)
let lunchTask = createMealTask(id: DiabetesTaskID.lunch, title: "์ ์ฌ ์์ฌ", hour: 12)
let dinnerTask = createMealTask(id: DiabetesTaskID.dinner, title: "์ ๋
์์ฌ", hour: 19)
// 4. ์ด๋ Task (์ฃผ 5ํ, 30๋ถ ์ด์)
let exerciseSchedule = OCKSchedule.weeklyAtTime(
weekday: [.monday, .tuesday, .wednesday, .thursday, .friday],
hours: 17,
minutes: 0,
start: Date(),
end: nil
)
var exerciseTask = OCKTask(id: DiabetesTaskID.exercise,
title: "์ ์ฐ์ ์ด๋",
carePlanUUID: nil,
schedule: exerciseSchedule)
exerciseTask.instructions = "30๋ถ ์ด์ ๊ฑท๊ธฐ, ์กฐ๊น
, ์์ ๊ฑฐ ๋ฑ์ ์ ์ฐ์ ์ด๋์ ํ์ธ์."
exerciseTask.impactsAdherence = true
// ๋ชจ๋ Task ์ ์ฅ
try await store.addTasks([glucoseTask, insulinTask, breakfastTask, lunchTask, dinnerTask, exerciseTask])
print("๋น๋จ๋ณ ๊ด๋ฆฌ ๊ณํ์ด ์์ฑ๋์์ต๋๋ค!")
}
private func createMealTask(id: String, title: String, hour: Int) -> OCKTask {
let schedule = OCKSchedule.dailyAtTime(hour: hour, minutes: 0, start: Date(), end: nil, text: nil)
var task = OCKTask(id: id, title: title, carePlanUUID: nil, schedule: schedule)
task.instructions = "์์ฌ ๋ด์ฉ๊ณผ ํ์ํ๋ฌผ ์ญ์ทจ๋์ ๊ธฐ๋กํ์ธ์."
return task
}
}
2๋จ๊ณ: ํ๋น ์ธก์ ํ๋ฉด ๊ตฌํ
ํ๋น ์ธก์ ๊ฒฐ๊ณผ๋ฅผ ์ ๋ ฅ๋ฐ๊ณ HealthKit์๋ ์ ์ฅํ๋ ํ๋ฉด์ ๋ง๋ค์ด๋ณด์.import UIKit
import CareKit
import HealthKit
class BloodGlucoseViewController: UIViewController {
let healthStore = HKHealthStore()
let careStore: OCKStore
var currentEvent: OCKAnyEvent?
let valueTextField: UITextField = {
let tf = UITextField()
tf.placeholder = "ํ๋น ์์น (mg/dL)"
tf.keyboardType = .numberPad
tf.borderStyle = .roundedRect
tf.font = .systemFont(ofSize: 18)
return tf
}()
let saveButton: UIButton = {
let btn = UIButton(type: .system)
btn.setTitle("์ ์ฅ", for: .normal)
btn.titleLabel?.font = .boldSystemFont(ofSize: 18)
btn.backgroundColor = .systemBlue
btn.setTitleColor(.white, for: .normal)
btn.layer.cornerRadius = 12
return btn
}()
init(careStore: OCKStore, event: OCKAnyEvent) {
self.careStore = careStore
self.currentEvent = event
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
setupUI()
saveButton.addTarget(self, action: #selector(saveButtonTapped), for: .touchUpInside)
}
private func setupUI() {
view.backgroundColor = .systemBackground
title = "ํ๋น ์ธก์ "
view.addSubview(valueTextField)
view.addSubview(saveButton)
valueTextField.translatesAutoresizingMaskIntoConstraints = false
saveButton.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
valueTextField.centerXAnchor.constraint(equalTo: view.centerXAnchor),
valueTextField.centerYAnchor.constraint(equalTo: view.centerYAnchor, constant: -50),
valueTextField.widthAnchor.constraint(equalToConstant: 250),
valueTextField.heightAnchor.constraint(equalToConstant: 50),
saveButton.topAnchor.constraint(equalTo: valueTextField.bottomAnchor, constant: 20),
saveButton.centerXAnchor.constraint(equalTo: view.centerXAnchor),
saveButton.widthAnchor.constraint(equalToConstant: 250),
saveButton.heightAnchor.constraint(equalToConstant: 50)
])
}
@objc private func saveButtonTapped() {
guard let valueText = valueTextField.text,
let glucoseValue = Double(valueText),
let event = currentEvent else {
showAlert(message: "์ฌ๋ฐ๋ฅธ ํ๋น ์์น๋ฅผ ์
๋ ฅํ์ธ์.")
return
}
Task {
do {
// CareKit์ ์ ์ฅ
try await saveToCareKit(value: glucoseValue, event: event)
// HealthKit์๋ ์ ์ฅ
try await saveToHealthKit(value: glucoseValue)
showAlert(message: "ํ๋น ์์น๊ฐ ์ ์ฅ๋์์ต๋๋ค!") {
self.dismiss(animated: true)
}
} catch {
showAlert(message: "์ ์ฅ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค: \(error.localizedDescription)")
}
}
}
private func saveToCareKit(value: Double, event: OCKAnyEvent) async throws {
let outcome = OCKOutcome(
taskUUID: event.task.uuid,
taskOccurrenceIndex: event.scheduleEvent.occurrence,
values: [OCKOutcomeValue(value, units: "mg/dL")]
)
try await careStore.addOutcome(outcome)
}
private func saveToHealthKit(value: Double) async throws {
guard let glucoseType = HKQuantityType.quantityType(forIdentifier: .bloodGlucose) else {
return
}
let quantity = HKQuantity(unit: HKUnit.gramUnit(with: .milli).unitDivided(by: .literUnit(with: .deci)), doubleValue: value)
let sample = HKQuantitySample(type: glucoseType, quantity: quantity, start: Date(), end: Date())
try await healthStore.save(sample)
}
private func showAlert(message: String, completion: (() -> Void)? = nil) {
let alert = UIAlertController(title: nil, message: message, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "ํ์ธ", style: .default) { _ in
completion?()
})
present(alert, animated: true)
}
}
3๋จ๊ณ: ํ๋น ์ถ์ด ๊ทธ๋ํ
๋ง์ง๋ง์ผ๋ก ์ง๋ 30์ผ๊ฐ์ ํ๋น ์ถ์ด๋ฅผ ๋ณด์ฌ์ฃผ๋ ๊ทธ๋ํ๋ฅผ ๋ง๋ค์! ๐class BloodGlucoseChartViewController: OCKCartesianChartViewController {
init(storeManager: OCKSynchronizedStoreManager) {
// ํ๋น ๋ฐ์ดํฐ ์๋ฆฌ์ฆ ์ค์
let dataConfig = OCKDataSeriesConfiguration(
taskID: DiabetesTaskID.bloodGlucose,
legendTitle: "ํ๋น (mg/dL)",
gradientStartColor: .systemRed,
gradientEndColor: .systemOrange,
markerSize: 6,
eventAggregator: .custom { events in
// ํ๋ฃจ ํ๊ท ํ๋น ๊ณ์ฐ
let values = events.compactMap { event -> Double? in
guard let value = event.outcome?.values.first?.doubleValue else { return nil }
return value
}
return values.isEmpty ? 0 : values.reduce(0, +) / Double(values.count)
}
)
super.init(plotType: .line, selectedDate: Date(), configurations: [dataConfig], storeManager: storeManager)
setupChart()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setupChart() {
// ์ฐจํธ ํค๋ ์ค์
chartView.headerView.titleLabel.text = "ํ๋น ์ถ์ด"
chartView.headerView.detailLabel.text = "์ต๊ทผ 30์ผ ํ๊ท "
// ์ ์ ๋ฒ์ ํ์ (70-140 mg/dL)
chartView.graphView.horizontalAxisMarkers = [70, 100, 140, 180]
// ์ํ ๊ตฌ๊ฐ ์์ ํ์
let normalRange = OCKCartesianGraphView.PlotRange(min: 70, max: 140)
chartView.graphView.selectedRangeColor = .systemGreen.withAlphaComponent(0.2)
}
}
๐ ๋ณด์๊ณผ ํ๋ผ์ด๋ฒ์ ๊ณ ๋ ค์ฌํญ
ํฌ์ค์ผ์ด ์ฑ์ ๊ฐ๋ฐํ ๋ ๊ฐ์ฅ ์ค์ํ ๊ฒ ๋ฐ๋ก ๋ณด์๊ณผ ํ๋ผ์ด๋ฒ์์ผ. ๊ฑด๊ฐ ๋ฐ์ดํฐ๋ ์ ๋ง ๋ฏผ๊ฐํ ๊ฐ์ธ์ ๋ณด๋๊น ๊ฐ๋ณํ ์ ๊ฒฝ ์จ์ผ ํด! ๐ก๏ธ
โ ๏ธ ๋ฐ๋์ ์ง์ผ์ผ ํ ๋ณด์ ์์น
1. ์ต์ ๊ถํ ์์น - ๊ผญ ํ์ํ ๋ฐ์ดํฐ๋ง ์์ฒญํ๊ธฐ
2. ๋ฐ์ดํฐ ์ํธํ - ์๋ฒ ์ ์ก ์ ๋ฐ๋์ HTTPS ์ฌ์ฉ
3. ๋ก์ปฌ ์ ์ฅ์ ๋ณดํธ - ๋ฏผ๊ฐํ ๋ฐ์ดํฐ๋ Keychain์ ์ ์ฅ
4. ์ฌ์ฉ์ ๋์ - ๋ฐ์ดํฐ ์ฌ์ฉ ๋ชฉ์ ์ ๋ช ํํ ์ค๋ช
5. ๋ฐ์ดํฐ ์ต์ํ - ํ์ ์ด์์ผ๋ก ๋ฐ์ดํฐ๋ฅผ ๋ณด๊ดํ์ง ์๊ธฐ
1. ์ต์ ๊ถํ ์์น - ๊ผญ ํ์ํ ๋ฐ์ดํฐ๋ง ์์ฒญํ๊ธฐ
2. ๋ฐ์ดํฐ ์ํธํ - ์๋ฒ ์ ์ก ์ ๋ฐ๋์ HTTPS ์ฌ์ฉ
3. ๋ก์ปฌ ์ ์ฅ์ ๋ณดํธ - ๋ฏผ๊ฐํ ๋ฐ์ดํฐ๋ Keychain์ ์ ์ฅ
4. ์ฌ์ฉ์ ๋์ - ๋ฐ์ดํฐ ์ฌ์ฉ ๋ชฉ์ ์ ๋ช ํํ ์ค๋ช
5. ๋ฐ์ดํฐ ์ต์ํ - ํ์ ์ด์์ผ๋ก ๋ฐ์ดํฐ๋ฅผ ๋ณด๊ดํ์ง ์๊ธฐ
HIPAA ์ค์ (๋ฏธ๊ตญ ์๋ฃ์ ๋ณด๋ณดํธ๋ฒ)
๋ง์ฝ ๋ฏธ๊ตญ ์์ฅ์ ํ๊ฒ์ผ๋ก ํ๋ค๋ฉด HIPAA(Health Insurance Portability and Accountability Act) ์ค์๊ฐ ํ์์ผ. ์ฃผ์ ์๊ตฌ์ฌํญ์:โข ํ์ ๋ฐ์ดํฐ์ ๊ธฐ๋ฐ์ฑ, ๋ฌด๊ฒฐ์ฑ, ๊ฐ์ฉ์ฑ ๋ณด์ฅ
โข ๋ฐ์ดํฐ ์ ๊ทผ ๋ก๊ทธ ๊ธฐ๋ก
โข ์ ๊ธฐ์ ์ธ ๋ณด์ ๊ฐ์ฌ
โข ๋ฐ์ดํฐ ์ ์ถ ์ 72์๊ฐ ๋ด ์ ๊ณ
โข ๋น์ฆ๋์ค ํํธ๋ ๊ณ์ฝ(BAA) ์ฒด๊ฒฐ
ํ๊ตญ์์๋ ๊ฐ์ธ์ ๋ณด๋ณดํธ๋ฒ๊ณผ ์๋ฃ๋ฒ์ ์ค์ํด์ผ ํ๊ณ , ์ ๋ฝ์ GDPR์ ๋ฐ๋ผ์ผ ํด. ๊ฐ ์ง์ญ์ ๋ฒ๊ท๋ฅผ ๊ผญ ํ์ธํ์! ๐
๋ฐ์ดํฐ ์ต๋ช ํ
๋ถ์์ด๋ ์ฐ๊ตฌ ๋ชฉ์ ์ผ๋ก ๋ฐ์ดํฐ๋ฅผ ์ฌ์ฉํ ๋๋ ๋ฐ๋์ ์ต๋ช ํํด์ผ ํด.struct AnonymizedHealthData {
let patientID: String // ์ค์ ID๊ฐ ์๋ ํด์๊ฐ
let age: Int
let gender: String
let measurements: [Double]
init(from patient: Patient) {
// ์ค์ ID๋ฅผ SHA-256์ผ๋ก ํด์
self.patientID = patient.id.sha256Hash
self.age = patient.age
self.gender = patient.gender
self.measurements = patient.measurements
}
}
extension String {
var sha256Hash: String {
// SHA-256 ํด์ ๊ตฌํ
// ์ค์ ๋ก๋ CryptoKit ์ฌ์ฉ ๊ถ์ฅ
return "hashed_\(self.hashValue)"
}
}
๐งช ํ ์คํธ์ ๋๋ฒ๊น
HealthKit ์๋ฎฌ๋ ์ดํฐ ํ ์คํธ
์ค์ ๊ธฐ๊ธฐ ์์ด๋ ์๋ฎฌ๋ ์ดํฐ์์ HealthKit์ ํ ์คํธํ ์ ์์ด! Xcode์ Health ์ฑ์์ ์ํ ๋ฐ์ดํฐ๋ฅผ ์ถ๊ฐํ๋ฉด ๋ผ.
๐ก ์๋ฎฌ๋ ์ดํฐ ํ
์คํธ ํ
1. ์ํ ๋ฐ์ดํฐ ์ถ๊ฐ
์๋ฎฌ๋ ์ดํฐ์ ๊ฑด๊ฐ ์ฑ์ ์ด๊ณ "Browse" โ ์ํ๋ ์นดํ ๊ณ ๋ฆฌ โ "Add Data"๋ก ํ ์คํธ ๋ฐ์ดํฐ๋ฅผ ์ง์ ์ ๋ ฅํ ์ ์์ด.
2. ์๋ ๋ฐ์ดํฐ ์์ฑ
์ฝ๋๋ก ํ ์คํธ ๋ฐ์ดํฐ๋ฅผ ์๋ ์์ฑํ๋ฉด ๋ ํธํด:
1. ์ํ ๋ฐ์ดํฐ ์ถ๊ฐ
์๋ฎฌ๋ ์ดํฐ์ ๊ฑด๊ฐ ์ฑ์ ์ด๊ณ "Browse" โ ์ํ๋ ์นดํ ๊ณ ๋ฆฌ โ "Add Data"๋ก ํ ์คํธ ๋ฐ์ดํฐ๋ฅผ ์ง์ ์ ๋ ฅํ ์ ์์ด.
2. ์๋ ๋ฐ์ดํฐ ์์ฑ
์ฝ๋๋ก ํ ์คํธ ๋ฐ์ดํฐ๋ฅผ ์๋ ์์ฑํ๋ฉด ๋ ํธํด:
func generateTestData() async throws {
let calendar = Calendar.current
let now = Date()
// ์ง๋ 30์ผ๊ฐ์ ๊ฑธ์ ์ ๋ฐ์ดํฐ ์์ฑ
for day in 0..<30 {
guard let date = calendar.date(byAdding: .day, value: -day, to: now) else { continue }
let steps = Double.random(in: 5000...15000)
let stepType = HKQuantityType.quantityType(forIdentifier: .stepCount)!
let quantity = HKQuantity(unit: .count(), doubleValue: steps)
let sample = HKQuantitySample(type: stepType, quantity: quantity, start: date, end: date)
try await healthStore.save(sample)
}
print("ํ
์คํธ ๋ฐ์ดํฐ ์์ฑ ์๋ฃ!")
}
Unit Testing
HealthKit๊ณผ CareKit ์ฝ๋๋ ์ ๋ ํ ์คํธ๋ฅผ ์์ฑํ ์ ์์ด. Mock ๊ฐ์ฒด๋ฅผ ์ฌ์ฉํ๋ฉด ์ค์ HealthKit ์์ด๋ ํ ์คํธ ๊ฐ๋ฅํด!import XCTest
@testable import MyHealthApp
class HealthKitManagerTests: XCTestCase {
var sut: HealthKitManager!
var mockHealthStore: MockHealthStore!
override func setUp() {
super.setUp()
mockHealthStore = MockHealthStore()
sut = HealthKitManager(healthStore: mockHealthStore)
}
func testFetchStepCount() async throws {
// Given
mockHealthStore.mockStepCount = 10000
// When
let steps = try await sut.getTodaySteps()
// Then
XCTAssertEqual(steps, 10000)
XCTAssertTrue(mockHealthStore.executeQueryCalled)
}
func testSaveWorkout() async throws {
// Given
let workout = createTestWorkout()
// When
try await sut.saveWorkout(workout)
// Then
XCTAssertTrue(mockHealthStore.saveCalled)
XCTAssertEqual(mockHealthStore.savedObjects.count, 1)
}
}
class MockHealthStore: HKHealthStore {
var mockStepCount: Double = 0
var executeQueryCalled = false
var saveCalled = false
var savedObjects: [HKObject] = []
override func execute(_ query: HKQuery) {
executeQueryCalled = true
// Mock ์๋ต ๋ฐํ
}
override func save(_ object: HKObject, withCompletion completion: @escaping (Bool, Error?) -> Void) {
saveCalled = true
savedObjects.append(object)
completion(true, nil)
}
}
๐ ๋ฐฐํฌ์ ์ฑ์คํ ์ด ์ ์ถ
์ฑ์คํ ์ด ๋ฆฌ๋ทฐ ๊ฐ์ด๋๋ผ์ธ
HealthKit์ ์ฌ์ฉํ๋ ์ฑ์ ์ผ๋ฐ ์ฑ๋ณด๋ค ์ฌ์ฌ๊ฐ ๊น๋ค๋ก์. ์ ํ์ด ํนํ ์ ๊ฒฝ ์ฐ๋ ๋ถ๋ถ๋ค์ด ์๊ฑฐ๋ ! ๐
๐ ์ฑ์คํ ์ด ์ฌ์ฌ ์ฒดํฌ๋ฆฌ์คํธ
โ ์๋ฃ ๊ธฐ๊ธฐ ๊ท์
์ฑ์ด ์๋ฃ ๊ธฐ๊ธฐ๋ก ๋ถ๋ฅ๋๋์ง ํ์ธ. ์ง๋จ์ด๋ ์น๋ฃ ๋ชฉ์ ์ด๋ผ๋ฉด FDA ์น์ธ์ด ํ์ํ ์ ์์ด.
โ ํ๋ผ์ด๋ฒ์ ์ ์ฑ
๋ช ํํ๊ณ ์ดํดํ๊ธฐ ์ฌ์ด ํ๋ผ์ด๋ฒ์ ์ ์ฑ ํ์. ์ด๋ค ๋ฐ์ดํฐ๋ฅผ ์์งํ๊ณ ์ด๋ป๊ฒ ์ฌ์ฉํ๋์ง ์์ธํ ์ค๋ช .
โ ๋ฐ์ดํฐ ์ฌ์ฉ ๋ชฉ์
Info.plist์ ์ฌ์ฉ ๋ชฉ์ ์ค๋ช ์ด ์ค์ ์ฑ ๊ธฐ๋ฅ๊ณผ ์ผ์นํด์ผ ํจ.
โ ๊ด๊ณ ๊ธ์ง
HealthKit ๋ฐ์ดํฐ๋ฅผ ๊ด๊ณ ๋ ๋ง์ผํ ์ ์ฌ์ฉํ๋ฉด ์ฆ์ ๋ฆฌ์ ! ์ ๋ ๊ธ์ง์ผ.
โ ๋ฐ์ดํฐ ํ๋งค ๊ธ์ง
๊ฑด๊ฐ ๋ฐ์ดํฐ๋ฅผ ์ 3์์๊ฒ ํ๋งคํ๊ฑฐ๋ ๊ณต์ ํ๋ ๊ฒ๋ ๊ธ์ง.
โ ์ํ์ ์ฃผ์ฅ ๊ฒ์ฆ
"์ด ์ฑ์ผ๋ก ๋น๋จ๋ณ์ ์น๋ฃํ ์ ์์ต๋๋ค" ๊ฐ์ ๊ฒ์ฆ๋์ง ์์ ์ํ์ ์ฃผ์ฅ ๊ธ์ง.
โ ์๋ฃ ๊ธฐ๊ธฐ ๊ท์
์ฑ์ด ์๋ฃ ๊ธฐ๊ธฐ๋ก ๋ถ๋ฅ๋๋์ง ํ์ธ. ์ง๋จ์ด๋ ์น๋ฃ ๋ชฉ์ ์ด๋ผ๋ฉด FDA ์น์ธ์ด ํ์ํ ์ ์์ด.
โ ํ๋ผ์ด๋ฒ์ ์ ์ฑ
๋ช ํํ๊ณ ์ดํดํ๊ธฐ ์ฌ์ด ํ๋ผ์ด๋ฒ์ ์ ์ฑ ํ์. ์ด๋ค ๋ฐ์ดํฐ๋ฅผ ์์งํ๊ณ ์ด๋ป๊ฒ ์ฌ์ฉํ๋์ง ์์ธํ ์ค๋ช .
โ ๋ฐ์ดํฐ ์ฌ์ฉ ๋ชฉ์
Info.plist์ ์ฌ์ฉ ๋ชฉ์ ์ค๋ช ์ด ์ค์ ์ฑ ๊ธฐ๋ฅ๊ณผ ์ผ์นํด์ผ ํจ.
โ ๊ด๊ณ ๊ธ์ง
HealthKit ๋ฐ์ดํฐ๋ฅผ ๊ด๊ณ ๋ ๋ง์ผํ ์ ์ฌ์ฉํ๋ฉด ์ฆ์ ๋ฆฌ์ ! ์ ๋ ๊ธ์ง์ผ.
โ ๋ฐ์ดํฐ ํ๋งค ๊ธ์ง
๊ฑด๊ฐ ๋ฐ์ดํฐ๋ฅผ ์ 3์์๊ฒ ํ๋งคํ๊ฑฐ๋ ๊ณต์ ํ๋ ๊ฒ๋ ๊ธ์ง.
โ ์ํ์ ์ฃผ์ฅ ๊ฒ์ฆ
"์ด ์ฑ์ผ๋ก ๋น๋จ๋ณ์ ์น๋ฃํ ์ ์์ต๋๋ค" ๊ฐ์ ๊ฒ์ฆ๋์ง ์์ ์ํ์ ์ฃผ์ฅ ๊ธ์ง.
์ฑ ๋ฉํ๋ฐ์ดํฐ ์์ฑ
์ฑ์คํ ์ด ์ค๋ช ์ ์์ฑํ ๋ ์ฃผ์ํ ์ ๋ค:โ ํผํด์ผ ํ ํํ
โข "์ง๋ณ ์น๋ฃ", "์ง๋จ", "์น์ "
โข "์์ฌ ๋์ฒด", "๋ณ์ ๋ฐฉ๋ฌธ ๋ถํ์"
โข "100% ์ ํ", "์ํ์ ์ผ๋ก ๊ฒ์ฆ๋จ" (์ค์ ๊ฒ์ฆ ์์ด)
โ ๊ถ์ฅํ๋ ํํ
โข "๊ฑด๊ฐ ๊ด๋ฆฌ ์ง์", "๊ฑด๊ฐ ์ถ์ "
โข "์๋ฃ์ง๊ณผ์ ์๋ด์ ๋ณด์"
โข "๊ฐ์ธ ๊ฑด๊ฐ ๋ฐ์ดํฐ ์๊ฐํ"
โข "์ํ ์ต๊ด ๊ฐ์ ๋์"
์ฌ๋ฅ๋ท ๊ฐ์ ํ๋ซํผ์์ ํฌ์ค์ผ์ด ์ฑ ๊ฐ๋ฐ ์๋น์ค๋ฅผ ์ ๊ณตํ๋ค๋ฉด, ์ด๋ฐ ๊ท์ ์ฌํญ๋ค์ ์ ์๊ณ ์์ด์ผ ํด๋ผ์ด์ธํธ์๊ฒ ์ ๋๋ก ๋ ์กฐ์ธ์ ํด์ค ์ ์์ด! ๐ผ
๐ ์ฑ๋ฅ ์ต์ ํ
๋ฐฐํฐ๋ฆฌ ํจ์จ์ฑ
๊ฑด๊ฐ ์ฑ์ ๋ฐฑ๊ทธ๋ผ์ด๋์์ ๊ณ์ ์คํ๋๋ ๊ฒฝ์ฐ๊ฐ ๋ง์์ ๋ฐฐํฐ๋ฆฌ ์๋ชจ๊ฐ ํฐ ์ด์์ผ. ์ต์ ํ ํ์ ์์๋ณด์! ๐class OptimizedHealthKitManager {
let healthStore = HKHealthStore()
private var observers: [HKObserverQuery] = []
// ๋ฐฐ์น ์
๋ฐ์ดํธ๋ก ํจ์จ์ฑ ํฅ์
func setupEfficientObserver() {
guard let stepType = HKQuantityType.quantityType(forIdentifier: .stepCount) else { return }
// ์ฆ์ ์
๋ฐ์ดํธ ๋์ ์๊ฐ๋ณ ์
๋ฐ์ดํธ ์ฌ์ฉ
healthStore.enableBackgroundDelivery(for: stepType, frequency: .hourly) { success, error in
if success {
print("์๊ฐ๋ณ ๋ฐฑ๊ทธ๋ผ์ด๋ ์
๋ฐ์ดํธ ํ์ฑํ")
}
}
}
// ์ฟผ๋ฆฌ ๊ฒฐ๊ณผ ์บ์ฑ
private var cachedSteps: (date: Date, value: Double)?
func getTodayStepsOptimized() async throws -> Double {
let today = Calendar.current.startOfDay(for: Date())
// ์บ์ ํ์ธ
if let cached = cachedSteps, Calendar.current.isDate(cached.date, inSameDayAs: today) {
return cached.value
}
// ์๋ก ๊ฐ์ ธ์ค๊ธฐ
let steps = try await fetchSteps()
cachedSteps = (today, steps)
return steps
}
private func fetchSteps() async throws -> Double {
// ์ค์ HealthKit ์ฟผ๋ฆฌ
return 0
}
// ์ฑ ์ข
๋ฃ ์ ๋ฆฌ์์ค ์ ๋ฆฌ
func cleanup() {
observers.forEach { healthStore.stop($0) }
observers.removeAll()
}
}
๋ฐ์ดํฐ ๋ก๋ฉ ์ต์ ํ
๋๋์ ๊ฑด๊ฐ ๋ฐ์ดํฐ๋ฅผ ๋ก๋ํ ๋๋ ํ์ด์ง๋ค์ด์ ์ ์ฌ์ฉํ์!func fetchHealthDataPaginated(limit: Int = 100, anchor: HKQueryAnchor? = nil) async throws -> (samples: [HKSample], newAnchor: HKQueryAnchor?) {
guard let heartRateType = HKQuantityType.quantityType(forIdentifier: .heartRate) else {
throw HealthKitError.typeNotAvailable
}
return try await withCheckedThrowingContinuation { continuation in
let query = HKAnchoredObjectQuery(
type: heartRateType,
predicate: nil,
anchor: anchor,
limit: limit
) { _, samples, _, newAnchor, error in
if let error = error {
continuation.resume(throwing: error)
} else {
continuation.resume(returning: (samples ?? [], newAnchor))
}
}
healthStore.execute(query)
}
}
// ์ฌ์ฉ ์์
func loadAllData() async {
var allSamples: [HKSample] = []
var currentAnchor: HKQueryAnchor? = nil
repeat {
do {
let result = try await fetchHealthDataPaginated(anchor: currentAnchor)
allSamples.append(contentsOf: result.samples)
currentAnchor = result.newAnchor
// UI ์
๋ฐ์ดํธ (์ ์ง์ ๋ก๋ฉ)
await MainActor.run {
updateUI(with: allSamples)
}
if result.samples.isEmpty {
break
}
} catch {
print("๋ฐ์ดํฐ ๋ก๋ฉ ์คํจ: \(error)")
break
}
} while true
}
๐ ๊ณ ๊ธ ๊ธฐ๋ฅ๊ณผ ํ
Apple Watch ์ฐ๋
์ ํ์์น ์ฑ์ ํจ๊ป ๊ฐ๋ฐํ๋ฉด ์ฌ์ฉ์ ๊ฒฝํ์ด ํจ์ฌ ์ข์์ ธ! โimport WatchConnectivity
class WatchConnectivityManager: NSObject, WCSessionDelegate {
static let shared = WatchConnectivityManager()
private var session: WCSession?
func setupSession() {
if WCSession.isSupported() {
session = WCSession.default
session?.delegate = self
session?.activate()
}
}
// ์์ดํฐ โ ์ ํ์์น๋ก ๋ฐ์ดํฐ ์ ์ก
func sendToWatch(data: [String: Any]) {
guard let session = session, session.isReachable else {
print("์์น์ ์ฐ๊ฒฐํ ์ ์์ต๋๋ค")
return
}
session.sendMessage(data, replyHandler: nil) { error in
print("์ ์ก ์คํจ: \(error.localizedDescription)")
}
}
// ์ ํ์์น โ ์์ดํฐ์ผ๋ก ๋ฐ์ดํฐ ์์
func session(_ session: WCSession, didReceiveMessage message: [String : Any]) {
if let heartRate = message["heartRate"] as? Double {
// ์ฌ๋ฐ์ ๋ฐ์ดํฐ ์ฒ๋ฆฌ
Task {
try await saveHeartRateToHealthKit(heartRate)
}
}
}
func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
if let error = error {
print("์ธ์
ํ์ฑํ ์คํจ: \(error.localizedDescription)")
} else {
print("์์น ์ฐ๊ฒฐ ์ฑ๊ณต!")
}
}
func sessionDidBecomeInactive(_ session: WCSession) {}
func sessionDidDeactivate(_ session: WCSession) {}
private func saveHeartRateToHealthKit(_ value: Double) async throws {
// HealthKit ์ ์ฅ ๋ก์ง
}
}
Siri ๋จ์ถ์ด ํตํฉ
Siri๋ก ๊ฑด๊ฐ ๋ฐ์ดํฐ๋ฅผ ๊ธฐ๋กํ ์ ์๊ฒ ํ๋ฉด ์ ๋ง ํธ๋ฆฌํด! ๐ฃ๏ธimport Intents
class HealthIntentHandler: NSObject, LogHealthDataIntentHandling {
func handle(intent: LogHealthDataIntent, completion: @escaping (LogHealthDataIntentResponse) -> Void) {
guard let dataType = intent.dataType,
let value = intent.value else {
completion(LogHealthDataIntentResponse(code: .failure, userActivity: nil))
return
}
Task {
do {
// ๋ฐ์ดํฐ ์ ์ฅ
try await saveHealthData(type: dataType, value: value.doubleValue)
let response = LogHealthDataIntentResponse(code: .success, userActivity: nil)
response.result = "\(dataType)์ด(๊ฐ) ๊ธฐ๋ก๋์์ต๋๋ค: \(value)"
completion(response)
} catch {
completion(LogHealthDataIntentResponse(code: .failure, userActivity: nil))
}
}
}
private func saveHealthData(type: String, value: Double) async throws {
// HealthKit ์ ์ฅ ๋ก์ง
}
}
์์ ฏ ์ง์
iOS 14๋ถํฐ ์์ ฏ์ ์ง์ํ๋ฉด์ ๊ฑด๊ฐ ๋ฐ์ดํฐ๋ฅผ ํ ํ๋ฉด์ ๋ฐ๋ก ํ์ํ ์ ์๊ฒ ๋์ด! ๐import WidgetKit
import SwiftUI
struct HealthWidgetView: View {
let entry: HealthEntry
var body: some View {
VStack(alignment: .leading, spacing: 8) {
Text("์ค๋์ ํ๋")
.font(.headline)
.foregroundColor(.white)
HStack {
VStack(alignment: .leading) {
Text("\(entry.steps)")
.font(.title)
.fontWeight(.bold)
.foregroundColor(.white)
Text("๊ฑธ์")
.font(.caption)
.foregroundColor(.white.opacity(0.8))
}
Spacer()
VStack(alignment: .leading) {
Text("\(entry.calories)")
.font(.title)
.fontWeight(.bold)
.foregroundColor(.white)
Text("์นผ๋ก๋ฆฌ")
.font(.caption)
.foregroundColor(.white.opacity(0.8))
}
}
ProgressView(value: Double(entry.steps) / 10000.0)
.tint(.green)
}
.padding()
.background(
LinearGradient(
colors: [Color.blue, Color.purple],
startPoint: .topLeading,
endPoint: .bottomTrailing
)
)
}
}
struct HealthEntry: TimelineEntry {
let date: Date
let steps: Int
let calories: Int
}
๐ ํ์ต ๋ฆฌ์์ค์ ์ปค๋ฎค๋ํฐ
๊ณต์ ๋ฌธ์
โข Apple Developer Documentation - HealthKit๊ณผ CareKit์ ๊ณต์ ๋ฌธ์. ๊ฐ์ฅ ์ ํํ๊ณ ์ต์ ์ ๋ณด๋ฅผ ์ป์ ์ ์์ด.โข WWDC ์ธ์ ๋น๋์ค - ๋งค๋ WWDC์์ ๊ฑด๊ฐ ๊ด๋ จ ์ธ์ ์ด ์ด๋ ค. ํนํ "What's New in HealthKit" ์ธ์ ์ ํ์!
โข Sample Code - ์ ํ์ด ์ ๊ณตํ๋ ์ํ ํ๋ก์ ํธ๋ค. ์ค์ ์ฝ๋๋ฅผ ๋ณผ ์ ์์ด์ ์ ๋ง ์ ์ฉํด.
์จ๋ผ์ธ ์ปค๋ฎค๋ํฐ
โข Apple Developer Forums - ์ ํ ์์ง๋์ด๋ค์ด ์ง์ ๋ต๋ณํด์ฃผ๋ ๊ณต์ ํฌ๋ผโข Stack Overflow - healthkit, carekit ํ๊ทธ๋ก ๊ฒ์ํ๋ฉด ์๋ง์ Q&A๋ฅผ ๋ณผ ์ ์์ด
โข Reddit r/iOSProgramming - iOS ๊ฐ๋ฐ์๋ค์ ํ๋ฐํ ์ปค๋ฎค๋ํฐ
โข ์ฌ๋ฅ๋ท - ํฌ์ค์ผ์ด ์ฑ ๊ฐ๋ฐ ๊ฒฝํ์ ๊ณต์ ํ๊ณ ๋ฐฐ์ธ ์ ์๋ ํ๋ซํผ! ๐ฏ
์ถ์ฒ ๋์
โข "iOS Health App Development with Swift" - HealthKit ๊ฐ๋ฐ์ ๋ฐ์ด๋ธโข "CareKit Programming Guide" - CareKit ์ฌํ ํ์ต
โข "Medical Device Software Development" - ์๋ฃ ์ํํธ์จ์ด ๊ท์ ์ดํด
๐ก ํ์ต ํ
ํฌ์ค์ผ์ด ์ฑ ๊ฐ๋ฐ์ ๋จ์ํ ์ฝ๋ฉ ์ค๋ ฅ๋ง์ผ๋ก๋ ๋ถ์กฑํด. ์๋ฃ ์ง์, ์ฌ์ฉ์ ๊ฒฝํ ๋์์ธ, ๋ฒ๊ท ์ดํด ๋ฑ ๋ค์ํ ์์ญ์ ์ง์์ด ํ์ํ๊ฑฐ๋ . ์ฒ์ฒํ ํ๋์ฉ ๋ฐฐ์๋๊ฐ๋ฉด์, ์ค์ ํ๋ก์ ํธ๋ฅผ ๋ง๋ค์ด๋ณด๋ ๊ฒ ๊ฐ์ฅ ์ข์ ํ์ต ๋ฐฉ๋ฒ์ด์ผ! ๐
ํฌ์ค์ผ์ด ์ฑ ๊ฐ๋ฐ์ ๋จ์ํ ์ฝ๋ฉ ์ค๋ ฅ๋ง์ผ๋ก๋ ๋ถ์กฑํด. ์๋ฃ ์ง์, ์ฌ์ฉ์ ๊ฒฝํ ๋์์ธ, ๋ฒ๊ท ์ดํด ๋ฑ ๋ค์ํ ์์ญ์ ์ง์์ด ํ์ํ๊ฑฐ๋ . ์ฒ์ฒํ ํ๋์ฉ ๋ฐฐ์๋๊ฐ๋ฉด์, ์ค์ ํ๋ก์ ํธ๋ฅผ ๋ง๋ค์ด๋ณด๋ ๊ฒ ๊ฐ์ฅ ์ข์ ํ์ต ๋ฐฉ๋ฒ์ด์ผ! ๐
๐ฏ ๋ง๋ฌด๋ฆฌํ๋ฉฐ
์, ์ฌ๊ธฐ๊น์ง ์ ๋ง ๊ธด ์ฌ์ ์ด์์ง? ๐ HealthKit๊ณผ CareKit์ ํ์ฉํ ํฌ์ค์ผ์ด ์ฑ ๊ฐ๋ฐ์ ๋ํด ์ ๋ง ๋ง์ ๊ฑธ ๋ค๋ค์ด!์ฐ๋ฆฌ๊ฐ ๋ฐฐ์ด ๋ด์ฉ์ ์ ๋ฆฌํด๋ณด๋ฉด:
โ HealthKit ๊ธฐ์ด
โข ๊ฑด๊ฐ ๋ฐ์ดํฐ์ ์ค์ ์ ์ฅ์ ์ญํ
โข ๋ค์ํ ๋ฐ์ดํฐ ํ์ (ํ๋, ์๋ฉด, ์์, ํ๋ ฅ์งํ ๋ฑ)
โข ๊ฐ๋ ฅํ ํ๋ผ์ด๋ฒ์์ ๋ณด์ ์์คํ
โข ๋ฐ์ดํฐ ์ฝ๊ธฐ/์ฐ๊ธฐ ๋ฐ ์ค์๊ฐ ๋ชจ๋ํฐ๋ง
โ CareKit ํ์ฉ
โข ์น๋ฃ ๊ณํ ๊ด๋ฆฌ (Task, Schedule, Outcome)
โข ๋ฏธ๋ฆฌ ๋ง๋ค์ด์ง UI ์ปดํฌ๋ํธ
โข ๋ฐ์ดํฐ ์๊ฐํ์ ์ธ์ฌ์ดํธ
โข ์๋ฃ์ง๊ณผ์ ์ฐ๊ฒฐ
โ ์ค์ ๊ฐ๋ฐ
โข HealthKit๊ณผ CareKit ์ฐ๋
โข ๋น๋จ๋ณ ๊ด๋ฆฌ ์ฑ ์์
โข ๋ณด์๊ณผ ํ๋ผ์ด๋ฒ์ ๊ณ ๋ ค์ฌํญ
โข ์ฑ๋ฅ ์ต์ ํ์ ๋ฐฐํฐ๋ฆฌ ํจ์จ์ฑ
โ ๊ณ ๊ธ ๊ธฐ๋ฅ
โข Apple Watch ์ฐ๋
โข Siri ๋จ์ถ์ด ํตํฉ
โข ์์ ฏ ์ง์
โข ์ฑ์คํ ์ด ์ ์ถ ๊ฐ์ด๋
ํฌ์ค์ผ์ด ์ฑ ๊ฐ๋ฐ์ ์ ๋ง ์๋ฏธ ์๋ ์ผ์ด์ผ. ์ฐ๋ฆฌ๊ฐ ๋ง๋ ์ฑ์ด ๋๊ตฐ๊ฐ์ ๊ฑด๊ฐ์ ์งํค๊ณ , ์ถ์ ์ง์ ํฅ์์ํฌ ์ ์๊ฑฐ๋ ! ๐ช
๋ฌผ๋ก ์ฒ์์๋ ์ด๋ ต๊ฒ ๋๊ปด์ง ์ ์์ด. HealthKit๊ณผ CareKit์ ๋ฐฉ๋ํ API, ๋ณต์กํ ์๋ฃ ๊ท์ , ๊น๋ค๋ก์ด ํ๋ผ์ด๋ฒ์ ์๊ตฌ์ฌํญ... ํ์ง๋ง ํ๋์ฉ ์ฐจ๊ทผ์ฐจ๊ทผ ๋ฐฐ์๋๊ฐ๋ค ๋ณด๋ฉด ์ด๋์ ๋ฉ์ง ํฌ์ค์ผ์ด ์ฑ์ ๋ง๋ค๊ณ ์๋ ์์ ์ ๋ฐ๊ฒฌํ๊ฒ ๋ ๊ฑฐ์ผ! ๐
๊ทธ๋ฆฌ๊ณ ํผ์ ๊ณต๋ถํ๊ธฐ ํ๋ค๋ค๋ฉด ์ฌ๋ฅ๋ท ๊ฐ์ ํ๋ซํผ์์ ๊ฒฝํ ์๋ ๊ฐ๋ฐ์๋ค์๊ฒ ๋ฐฐ์ฐ๊ฑฐ๋, ๋ฐ๋๋ก ์์ ์ ์ง์์ ๊ณต์ ํ๋ฉด์ ์ฑ์ฅํ ์๋ ์์ด. ํจ๊ป ๋ฐฐ์ฐ๊ณ ์ฑ์ฅํ๋ ๊ฒ ๊ฐ์ฅ ๋น ๋ฅธ ๊ธธ์ด๋๊น! ๐ค
์, ์ด์ ๋น์ ์ฐจ๋ก์ผ! ์ค๋ ๋ฐฐ์ด ๋ด์ฉ์ ๋ฐํ์ผ๋ก ์์ ๋ง์ ํฌ์ค์ผ์ด ์ฑ์ ๋ง๋ค์ด๋ณด์. ์์ ํ๋ก์ ํธ๋ถํฐ ์์ํด์ ์ ์ ๊ธฐ๋ฅ์ ์ถ๊ฐํด๋๊ฐ๋ฉด ๋ผ. ๋๊ฐ ์์? ๋น์ ์ด ๋ง๋ ์ฑ์ด ์๋ฐฑ๋ง ๋ช ์ ๊ฑด๊ฐ์ ์ฑ ์์ง๋ ๋ ์ด ์ฌ์ง! ๐
๊ฑด๊ฐํ ์ฝ๋ฉ ๋๊ธธ ๋ฐ๋ผ! Happy Coding! ๐ปโจ
๐ ์ถํํฉ๋๋ค!
HealthKit๊ณผ CareKit ๋ง์คํฐ ๊ณผ์ ์ ์๋ฃํ์
จ์ต๋๋ค!
์ด์ ๋น์ ๋ ํฌ์ค์ผ์ด ์ฑ ๊ฐ๋ฐ์์
๋๋ค! ๐ช
๊ด๋ จ ํค์๋
๋๊ธ 0
์ง์์ธ์ ์ฒ - ์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
- ์ ์๊ถ ๋ฐ ์์ ๊ถ: ๋ณธ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ๋ ์ AI ๊ธฐ์ ๋ก ์์ฑ๋์์ผ๋ฉฐ, ๋ํ๋ฏผ๊ตญ ์ ์๊ถ๋ฒ ๋ฐ ๊ตญ์ ์ ์๊ถ ํ์ฝ์ ์ํด ๋ณดํธ๋ฉ๋๋ค.
- AI ์์ฑ ์ปจํ ์ธ ์ ๋ฒ์ ์ง์: ๋ณธ AI ์์ฑ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ์ง์ ์ฐฝ์๋ฌผ๋ก ์ธ์ ๋๋ฉฐ, ๊ด๋ จ ๋ฒ๊ท์ ๋ฐ๋ผ ์ ์๊ถ ๋ณดํธ๋ฅผ ๋ฐ์ต๋๋ค.
- ์ฌ์ฉ ์ ํ: ์ฌ๋ฅ๋ท์ ๋ช ์์ ์๋ฉด ๋์ ์์ด ๋ณธ ์ปจํ ์ธ ๋ฅผ ๋ณต์ , ์์ , ๋ฐฐํฌ, ๋๋ ์์ ์ ์ผ๋ก ํ์ฉํ๋ ํ์๋ ์๊ฒฉํ ๊ธ์ง๋ฉ๋๋ค.
- ๋ฐ์ดํฐ ์์ง ๊ธ์ง: ๋ณธ ์ปจํ ์ธ ์ ๋ํ ๋ฌด๋จ ์คํฌ๋ํ, ํฌ๋กค๋ง, ๋ฐ ์๋ํ๋ ๋ฐ์ดํฐ ์์ง์ ๋ฒ์ ์ ์ฌ์ ๋์์ด ๋ฉ๋๋ค.
- AI ํ์ต ์ ํ: ์ฌ๋ฅ๋ท์ AI ์์ฑ ์ปจํ ์ธ ๋ฅผ ํ AI ๋ชจ๋ธ ํ์ต์ ๋ฌด๋จ ์ฌ์ฉํ๋ ํ์๋ ๊ธ์ง๋๋ฉฐ, ์ด๋ ์ง์ ์ฌ์ฐ๊ถ ์นจํด๋ก ๊ฐ์ฃผ๋ฉ๋๋ค.

๋๊ธ ์์ฑ
์ด ๊ธ์ ๋ํ ์ฌ๋ฌ๋ถ์ ์๊ฐ์ ๋ค๋ ค์ฃผ์ธ์
๋ก๊ทธ์ธ์ด ํ์ํฉ๋๋ค
๋๊ธ์ ์์ฑํ๋ ค๋ฉด ๋จผ์ ๋ก๊ทธ์ธํด์ฃผ์ธ์.