์ฝ˜ํ…์ธ  ๋Œ€ํ‘œ ์ด๋ฏธ์ง€ - ๐Ÿฅ HealthKit๊ณผ CareKit์œผ๋กœ ๋งŒ๋“œ๋Š” ํ—ฌ์Šค์ผ€์–ด ์•ฑ

๐Ÿฅ HealthKit๊ณผ CareKit์œผ๋กœ ๋งŒ๋“œ๋Š” ํ—ฌ์Šค์ผ€์–ด ์•ฑ

์• ํ”Œ ์ƒํƒœ๊ณ„์—์„œ ๊ฑด๊ฐ• ๋ฐ์ดํ„ฐ๋ฅผ ๋‹ค๋ฃจ๋Š” ์™„๋ฒฝ ๊ฐ€์ด๋“œ

๐ŸŽฏ ์™œ ์ง€๊ธˆ ํ—ฌ์Šค์ผ€์–ด ์•ฑ์ธ๊ฐ€?

์š”์ฆ˜ ์ฃผ๋ณ€์„ ๋‘˜๋Ÿฌ๋ณด๋ฉด ์†๋ชฉ์— ์• ํ”Œ์›Œ์น˜๋ฅผ ์ฐจ๊ณ  ์žˆ๋Š” ์‚ฌ๋žŒ๋“ค์ด ์ •๋ง ๋งŽ์•„์กŒ์ง€? ๐ŸŽโŒš ๊ทธ๋ƒฅ ์‹œ๊ฐ„๋งŒ ๋ณด๋Š” ๊ฒŒ ์•„๋‹ˆ๋ผ ์‹ฌ๋ฐ•์ˆ˜, ๊ฑธ์Œ ์ˆ˜, ์ˆ˜๋ฉด ํŒจํ„ด๊นŒ์ง€ ์ฒดํฌํ•˜๋ฉด์„œ ์ž๊ธฐ ๊ฑด๊ฐ•์„ ๊ด€๋ฆฌํ•˜๋Š” ์‹œ๋Œ€๊ฐ€ ๋์–ด. ์ด๋Ÿฐ ํ๋ฆ„ ์†์—์„œ ํ—ฌ์Šค์ผ€์–ด ์•ฑ ๊ฐœ๋ฐœ์€ ๋‹จ์ˆœํ•œ ํŠธ๋ Œ๋“œ๊ฐ€ ์•„๋‹ˆ๋ผ ํ•„์ˆ˜ ์—ญ๋Ÿ‰์ด ๋˜์–ด๊ฐ€๊ณ  ์žˆ์–ด.

์• ํ”Œ์€ ์ด๋ฏธ ์˜ค๋ž˜์ „๋ถ€ํ„ฐ ๊ฑด๊ฐ• ๋ฐ์ดํ„ฐ์˜ ์ค‘์š”์„ฑ์„ ์•Œ๊ณ  ์žˆ์—ˆ๊ณ , ๊ทธ๋ž˜์„œ ๋งŒ๋“  ๊ฒŒ ๋ฐ”๋กœ HealthKit๊ณผ CareKit์ด์•ผ. ์ด ๋‘ ํ”„๋ ˆ์ž„์›Œํฌ๋Š” iOS ์ƒํƒœ๊ณ„์—์„œ ๊ฑด๊ฐ• ๊ด€๋ จ ์•ฑ์„ ๋งŒ๋“ค ๋•Œ ์ ˆ๋Œ€ ๋นผ๋†“์„ ์ˆ˜ ์—†๋Š” ํ•ต์‹ฌ ๋„๊ตฌ๋“ค์ด์ง€. ๐Ÿ’ช

ํŠนํžˆ ์žฌ๋Šฅ๋„ท ๊ฐ™์€ ํ”Œ๋žซํผ์—์„œ ํ—ฌ์Šค์ผ€์–ด ์•ฑ ๊ฐœ๋ฐœ ์žฌ๋Šฅ์„ ๊ณต์œ ํ•˜๊ฑฐ๋‚˜ ๋ฐฐ์šฐ๋ ค๋Š” ์‚ฌ๋žŒ๋“ค์ด ๋Š˜์–ด๋‚˜๊ณ  ์žˆ๋Š”๋ฐ, ๊ทธ๋งŒํผ ์‹œ์žฅ์˜ ์ˆ˜์š”๊ฐ€ ํฌ๋‹ค๋Š” ๋œป์ด๊ธฐ๋„ ํ•ด. ์ž, ๊ทธ๋Ÿผ ๋ณธ๊ฒฉ์ ์œผ๋กœ ์ด ๋‘ ํ”„๋ ˆ์ž„์›Œํฌ๊ฐ€ ๋ญ”์ง€, ์–ด๋–ป๊ฒŒ ํ™œ์šฉํ•˜๋Š”์ง€ ์นœ๊ตฌ์ฒ˜๋Ÿผ ํŽธํ•˜๊ฒŒ ์•Œ์•„๋ณด์ž! ๐Ÿš€

๐Ÿ“ฑ HealthKit์ด ๋ญ๊ธธ๋ž˜?

HealthKit์˜ ์ •์ฒด

HealthKit์€ 2014๋…„ iOS 8๊ณผ ํ•จ๊ป˜ ์ฒ˜์Œ ๋“ฑ์žฅํ•œ ํ”„๋ ˆ์ž„์›Œํฌ์•ผ. ๊ฐ„๋‹จํžˆ ๋งํ•˜๋ฉด ์•„์ดํฐ๊ณผ ์• ํ”Œ์›Œ์น˜์— ์ €์žฅ๋œ ๊ฑด๊ฐ• ๋ฐ์ดํ„ฐ๋ฅผ ์•ˆ์ „ํ•˜๊ฒŒ ์ฝ๊ณ  ์“ธ ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์ฃผ๋Š” ์ค‘์•™ ์ €์žฅ์†Œ๋ผ๊ณ  ์ƒ๊ฐํ•˜๋ฉด ๋ผ. ๐Ÿฆ

์˜ˆ๋ฅผ ๋“ค์–ด๋ณผ๊ฒŒ. ๋„ˆ๊ฐ€ ์•„์นจ์— ์กฐ๊น…์„ ํ•˜๋ฉด์„œ ๋‚˜์ดํ‚ค ๋Ÿฌ๋‹ ์•ฑ์„ ์‚ฌ์šฉํ–ˆ์–ด. ๊ทธ๋Ÿผ ์ด ์•ฑ์€ HealthKit์— ๋„ˆ์˜ ์šด๋™ ๋ฐ์ดํ„ฐ๋ฅผ ์ €์žฅํ•ด. ๊ทธ๋ฆฌ๊ณ  ์ ์‹ฌ๋•Œ ๋‹ค๋ฅธ ๊ฑด๊ฐ• ๊ด€๋ฆฌ ์•ฑ์„ ์—ด๋ฉด, ๊ทธ ์•ฑ๋„ HealthKit์—์„œ ์•„์นจ ์šด๋™ ๋ฐ์ดํ„ฐ๋ฅผ ์ฝ์–ด์˜ฌ ์ˆ˜ ์žˆ์ง€. ์ด๋ ‡๊ฒŒ ์—ฌ๋Ÿฌ ์•ฑ๋“ค์ด ํ•˜๋‚˜์˜ ๊ฑด๊ฐ• ๋ฐ์ดํ„ฐ ์ €์žฅ์†Œ๋ฅผ ๊ณต์œ ํ•˜๋Š” ๊ฑฐ์•ผ. ๐Ÿ”„

๐ŸŽฏ HealthKit์˜ ํ•ต์‹ฌ ๊ธฐ๋Šฅ

1. ๋ฐ์ดํ„ฐ ํ†ตํ•ฉ - ์—ฌ๋Ÿฌ ์•ฑ๊ณผ ๊ธฐ๊ธฐ์˜ ๊ฑด๊ฐ• ๋ฐ์ดํ„ฐ๋ฅผ ํ•œ๊ณณ์— ๋ชจ์Œ
2. ํ”„๋ผ์ด๋ฒ„์‹œ ๋ณดํ˜ธ - ์‚ฌ์šฉ์ž๊ฐ€ ์ง์ ‘ ์–ด๋–ค ๋ฐ์ดํ„ฐ๋ฅผ ๊ณต์œ ํ• ์ง€ ์„ ํƒ
3. ์‹ค์‹œ๊ฐ„ ๋™๊ธฐํ™” - ์• ํ”Œ์›Œ์น˜, ์•„์ดํฐ, ์•„์ดํŒจ๋“œ ๊ฐ„ ์ž๋™ ๋™๊ธฐํ™”
4. ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์—…๋ฐ์ดํŠธ - ์•ฑ์ด ๊บผ์ ธ์žˆ์–ด๋„ ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘ ๊ฐ€๋Šฅ

HealthKit์ด ๋‹ค๋ฃจ๋Š” ๋ฐ์ดํ„ฐ ์ข…๋ฅ˜

HealthKit์ด ๊ด€๋ฆฌํ•˜๋Š” ๊ฑด๊ฐ• ๋ฐ์ดํ„ฐ๋Š” ์ •๋ง ๋‹ค์–‘ํ•ด. ํฌ๊ฒŒ ๋ช‡ ๊ฐ€์ง€ ์นดํ…Œ๊ณ ๋ฆฌ๋กœ ๋‚˜๋ˆŒ ์ˆ˜ ์žˆ์–ด:

๐Ÿ“Š ์‹ ์ฒด ์ธก์ • ๋ฐ์ดํ„ฐ (Body Measurements)
ํ‚ค, ๋ชธ๋ฌด๊ฒŒ, ์ฒด์งˆ๋Ÿ‰์ง€์ˆ˜(BMI), ์ฒด์ง€๋ฐฉ๋ฅ  ๊ฐ™์€ ๊ธฐ๋ณธ์ ์ธ ์‹ ์ฒด ์ •๋ณด๋“ค์ด์•ผ. ์ด๋Ÿฐ ๋ฐ์ดํ„ฐ๋Š” ๋ณดํ†ต ์ˆ˜๋™์œผ๋กœ ์ž…๋ ฅํ•˜๊ฑฐ๋‚˜ ์Šค๋งˆํŠธ ์ฒด์ค‘๊ณ„ ๊ฐ™์€ ๊ธฐ๊ธฐ์—์„œ ์ž๋™์œผ๋กœ ๋ฐ›์•„์˜ค์ง€.

๐Ÿ’“ ํ™œ๋ ฅ ์ง•ํ›„ (Vital Signs)
์‹ฌ๋ฐ•์ˆ˜, ํ˜ˆ์••, ์ฒด์˜จ, ํ˜ธํก์ˆ˜, ํ˜ˆ์ค‘ ์‚ฐ์†Œํฌํ™”๋„ ๋“ฑ ์ƒ๋ช…๊ณผ ์ง๊ฒฐ๋œ ์ค‘์š”ํ•œ ๋ฐ์ดํ„ฐ๋“ค์ด์•ผ. ์• ํ”Œ์›Œ์น˜๊ฐ€ ํŠนํžˆ ์ด ๋ถ€๋ถ„์—์„œ ๊ฐ•๋ ฅํ•œ๋ฐ, ์‹ฌ๋ฐ•์ˆ˜๋Š” ๊ฑฐ์˜ ์‹ค์‹œ๊ฐ„์œผ๋กœ ์ธก์ •๋˜๊ฑฐ๋“ . ๐Ÿซ€

๐Ÿƒ ํ™œ๋™ ๋ฐ์ดํ„ฐ (Activity Data)
๊ฑธ์Œ ์ˆ˜, ์ด๋™ ๊ฑฐ๋ฆฌ, ์˜ค๋ฅธ ์ธต์ˆ˜, ํ™œ๋™ ์นผ๋กœ๋ฆฌ, ์šด๋™ ์‹œ๊ฐ„ ๋“ฑ ์‹ ์ฒด ํ™œ๋™๊ณผ ๊ด€๋ จ๋œ ๋ชจ๋“  ๊ฒƒ๋“ค. ์ด๊ฒŒ ๋ฐ”๋กœ ์• ํ”Œ์›Œ์น˜์˜ 'ํ™œ๋™' ๋ง์„ ์ฑ„์šฐ๋Š” ๋ฐ์ดํ„ฐ์•ผ! ๐Ÿ”ฅ

๐Ÿ˜ด ์ˆ˜๋ฉด ๋ฐ์ดํ„ฐ (Sleep Analysis)
์ˆ˜๋ฉด ์‹œ๊ฐ„, ์ˆ˜๋ฉด ๋‹จ๊ณ„(์–•์€ ์ˆ˜๋ฉด, ๊นŠ์€ ์ˆ˜๋ฉด, REM ์ˆ˜๋ฉด), ์ˆ˜๋ฉด์˜ ์งˆ ๋“ฑ์„ ์ถ”์ ํ•ด. ์š”์ฆ˜์€ ์ˆ˜๋ฉด ๊ฑด๊ฐ•์ด ์ •๋ง ์ค‘์š”ํ•˜๊ฒŒ ์—ฌ๊ฒจ์ง€์ž–์•„?

๐ŸŽ ์˜์–‘ ๋ฐ์ดํ„ฐ (Nutrition)
์„ญ์ทจํ•œ ์นผ๋กœ๋ฆฌ, ๋‹จ๋ฐฑ์งˆ, ํƒ„์ˆ˜ํ™”๋ฌผ, ์ง€๋ฐฉ, ๋น„ํƒ€๋ฏผ, ๋ฏธ๋„ค๋ž„ ๋“ฑ ์˜์–‘์†Œ ์ •๋ณด. ๋‹ค์ด์–ดํŠธ ์•ฑ์ด๋‚˜ ์‹๋‹จ ๊ด€๋ฆฌ ์•ฑ์—์„œ ์ฃผ๋กœ ์‚ฌ์šฉํ•ด.

๐Ÿง˜ ๋งˆ์Œ์ฑ™๊น€ (Mindfulness)
๋ช…์ƒ ์‹œ๊ฐ„, ์ŠคํŠธ๋ ˆ์Šค ์ˆ˜์ค€ ๊ฐ™์€ ์ •์‹  ๊ฑด๊ฐ• ๊ด€๋ จ ๋ฐ์ดํ„ฐ๋„ ํฌํ•จ๋ผ. ์š”์ฆ˜ ๋ฉ˜ํƒˆ ํ—ฌ์Šค๊ฐ€ ์ค‘์š”ํ•ด์ง€๋ฉด์„œ ์ด ์นดํ…Œ๊ณ ๋ฆฌ๋„ ์ฃผ๋ชฉ๋ฐ›๊ณ  ์žˆ์–ด. ๐Ÿง 
HealthKit ์ค‘์•™ ๋ฐ์ดํ„ฐ ์ €์žฅ์†Œ ๐Ÿ’“ ๐Ÿƒ ๐Ÿ˜ด ๐ŸŽ ๐Ÿง˜ ์šด๋™ ์•ฑ ๐Ÿ‹๏ธ ์‹๋‹จ ์•ฑ ๐Ÿฅ— ์ˆ˜๋ฉด ์•ฑ ๐ŸŒ™ ๋ช…์ƒ ์•ฑ ๐Ÿง˜โ€โ™€๏ธ ๋ชจ๋“  ๊ฑด๊ฐ• ์•ฑ์ด ํ•˜๋‚˜์˜ ๋ฐ์ดํ„ฐ ์ €์žฅ์†Œ๋ฅผ ๊ณต์œ ํ•ฉ๋‹ˆ๋‹ค

๐Ÿ” HealthKit์˜ ํ”„๋ผ์ด๋ฒ„์‹œ์™€ ๋ณด์•ˆ

๊ฑด๊ฐ• ๋ฐ์ดํ„ฐ๋Š” ์ •๋ง ๋ฏผ๊ฐํ•œ ๊ฐœ์ธ์ •๋ณด์ž–์•„? ๊ทธ๋ž˜์„œ ์• ํ”Œ์€ HealthKit์— ์—„์ฒญ๋‚˜๊ฒŒ ๊ฐ•๋ ฅํ•œ ๋ณด์•ˆ ์‹œ์Šคํ…œ์„ ๊ตฌ์ถ•ํ•ด๋†จ์–ด. ๐Ÿ›ก๏ธ

๐Ÿ’ก HealthKit ๋ณด์•ˆ์˜ ํ•ต์‹ฌ ์›์น™

1. ๋ช…์‹œ์  ๋™์˜ - ์•ฑ์ด ๊ฑด๊ฐ• ๋ฐ์ดํ„ฐ์— ์ ‘๊ทผํ•˜๋ ค๋ฉด ๋ฐ˜๋“œ์‹œ ์‚ฌ์šฉ์ž์˜ ํ—ˆ๊ฐ€๋ฅผ ๋ฐ›์•„์•ผ ํ•ด. ๊ทธ๊ฒƒ๋„ ๋ฐ์ดํ„ฐ ํƒ€์ž…๋ณ„๋กœ ๊ฐ๊ฐ!

2. ์ฝ๊ธฐ/์“ฐ๊ธฐ ๊ถŒํ•œ ๋ถ„๋ฆฌ - ๋ฐ์ดํ„ฐ๋ฅผ ์ฝ๋Š” ๊ถŒํ•œ๊ณผ ์“ฐ๋Š” ๊ถŒํ•œ์ด ์™„์ „ํžˆ ๋ถ„๋ฆฌ๋˜์–ด ์žˆ์–ด.

3. ํด๋ผ์šฐ๋“œ ๋™๊ธฐํ™” ์•”ํ˜ธํ™” - iCloud๋ฅผ ํ†ตํ•ด ๋™๊ธฐํ™”๋  ๋•Œ ์—”๋“œํˆฌ์—”๋“œ ์•”ํ˜ธํ™”๊ฐ€ ์ ์šฉ๋ผ.

4. ์•ฑ ์ƒŒ๋“œ๋ฐ•์Šค - ๊ฐ ์•ฑ์€ ์ž๊ธฐ๊ฐ€ ๋งŒ๋“  ๋ฐ์ดํ„ฐ๋งŒ ์ˆ˜์ •ํ•  ์ˆ˜ ์žˆ๊ณ , ๋‹ค๋ฅธ ์•ฑ์˜ ๋ฐ์ดํ„ฐ๋Š” ์ฝ๊ธฐ๋งŒ ๊ฐ€๋Šฅํ•ด.
์‹ค์ œ๋กœ ์•ฑ์„ ๊ฐœ๋ฐœํ•  ๋•Œ ์‚ฌ์šฉ์ž์—๊ฒŒ ๊ถŒํ•œ์„ ์š”์ฒญํ•˜๋Š” ๊ณผ์ •์ด ๊ฝค ๊นŒ๋‹ค๋กœ์›Œ. ํ•˜์ง€๋งŒ ์ด๊ฒŒ ๋ฐ”๋กœ ์‚ฌ์šฉ์ž์˜ ๊ฑด๊ฐ• ์ •๋ณด๋ฅผ ์ง€ํ‚ค๋Š” ๋ฐฉ๋ฒ•์ด๋‹ˆ๊นŒ ๋‹น์—ฐํ•œ ๊ฑฐ์ง€! ๐Ÿ˜Š

๐Ÿ’ป HealthKit ๊ฐœ๋ฐœ ์‹œ์ž‘ํ•˜๊ธฐ

๊ฐœ๋ฐœ ํ™˜๊ฒฝ ์„ค์ •

์ž, ์ด์ œ ์‹ค์ œ๋กœ HealthKit์„ ์‚ฌ์šฉํ•ด์„œ ์•ฑ์„ ๋งŒ๋“ค์–ด๋ณด์ž! ๋จผ์ € ๊ฐœ๋ฐœ ํ™˜๊ฒฝ๋ถ€ํ„ฐ ์„ธํŒ…ํ•ด์•ผ ํ•ด.

๐Ÿ“‹ ํ•„์š”ํ•œ ๊ฒƒ๋“ค

โ€ข 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() ๋ฉ”์„œ๋“œ๋กœ ํ•ญ์ƒ ์ฒดํฌํ•˜๋Š” ์Šต๊ด€์„ ๋“ค์ด์ž.

๊ถŒํ•œ ์š”์ฒญํ•˜๊ธฐ

์ด์ œ ์‚ฌ์šฉ์ž์—๊ฒŒ ๊ฑด๊ฐ• ๋ฐ์ดํ„ฐ ์ ‘๊ทผ ๊ถŒํ•œ์„ ์š”์ฒญํ•ด๋ณด์ž. ๋จผ์ € ์–ด๋–ค ๋ฐ์ดํ„ฐ ํƒ€์ž…์— ์ ‘๊ทผํ•  ๊ฑด์ง€ ์ •์˜ํ•ด์•ผ ํ•ด.

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)
    }
}

์˜ˆ๋ฅผ ๋“ค์–ด ์šด๋™ ์ค‘์— ์ธก์ •ํ•œ ์—ฌ๋Ÿฌ ์‹ฌ๋ฐ•์ˆ˜ ๋ฐ์ดํ„ฐ๋ฅผ ๋ชจ์•„๋’€๋‹ค๊ฐ€ ํ•œ ๋ฒˆ์— ์ €์žฅํ•  ์ˆ˜ ์žˆ์ง€. ์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ๋ฐฐํ„ฐ๋ฆฌ๋„ ์ ˆ์•ฝ๋˜๊ณ  ์„ฑ๋Šฅ๋„ ์ข‹์•„์ ธ! ๐Ÿ”‹
HealthKit ๋ฐ์ดํ„ฐ ํ๋ฆ„ โŒš ์• ํ”Œ์›Œ์น˜ ๐Ÿ“ฑ ์•„์ดํฐ ๐Ÿฅ ์˜๋ฃŒ๊ธฐ๊ธฐ โœ๏ธ ์ˆ˜๋™์ž…๋ ฅ ๐Ÿ“ฒ ์จ๋“œํŒŒํ‹ฐ ์•ฑ HealthKit Store ์ค‘์•™ ๊ฑด๊ฐ• ๋ฐ์ดํ„ฐ ์ €์žฅ์†Œ ๐Ÿ’“ ์‹ฌ๋ฐ•์ˆ˜ ๐Ÿƒ ํ™œ๋™๋Ÿ‰ ๐Ÿ˜ด ์ˆ˜๋ฉด ๐ŸŽ ์˜์–‘ ๐Ÿง˜ ๋งˆ์Œ์ฑ™๊น€ ๐Ÿ“Š ํ†ต๊ณ„ ํ”ผํŠธ๋‹ˆ์Šค ์•ฑ ๐Ÿ‹๏ธโ€โ™€๏ธ ๊ฑด๊ฐ• ๋Œ€์‹œ๋ณด๋“œ ๐Ÿ“Š ์˜๋ฃŒ ์•ฑ ๐Ÿฅ ๋ชจ๋“  ๋ฐ์ดํ„ฐ๊ฐ€ HealthKit์„ ํ†ตํ•ด ์•ˆ์ „ํ•˜๊ฒŒ ๊ณต์œ ๋ฉ๋‹ˆ๋‹ค

๐Ÿ”” ์‹ค์‹œ๊ฐ„ ๋ฐ์ดํ„ฐ ๋ชจ๋‹ˆํ„ฐ๋ง

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์ด ๋‚˜์™”์–ด. ์ง€๊ธˆ ์šฐ๋ฆฌ๊ฐ€ ๋ฐฐ์šธ ๊ฑด ์ด ์ตœ์‹  ๋ฒ„์ „์ด์•ผ! โœจ

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 ์น˜๋ฃŒ ๊ณ„ํš ๊ตฌ์กฐ Care Plan ์ „์ฒด ์น˜๋ฃŒ ๊ณ„ํš ๐Ÿ’Š ์•ฝ ๋ณต์šฉ ๋งค์ผ ์•„์นจ 8์‹œ ์ €๋… 8์‹œ ๐Ÿƒ ์šด๋™ํ•˜๊ธฐ ์ฃผ 3ํšŒ 30๋ถ„ ์ด์ƒ ๐Ÿฉบ ํ˜ˆ์•• ์ธก์ • ๋งค์ผ ์•„์นจ ๊ณต๋ณต ์ƒํƒœ ๐Ÿ˜Š ๊ธฐ๋ถ„ ๊ธฐ๋ก ํ•˜๋ฃจ 3๋ฒˆ ์•„์นจ/์ ์‹ฌ/์ €๋… Outcomes (์‹คํ–‰ ๊ฒฐ๊ณผ) 2024.01.15 โœ“ ์•„์นจ ๋ณต์šฉ โœ“ ์ €๋… ๋ณต์šฉ ์ค€์ˆ˜์œจ: 100% ์—ฐ์† 7์ผ ๋‹ฌ์„ฑ! 2024.01.15 โœ“ ์กฐ๊น… 45๋ถ„ ๊ฑฐ๋ฆฌ: 5.2km ์นผ๋กœ๋ฆฌ: 320kcal ๋ชฉํ‘œ ๋‹ฌ์„ฑ! 2024.01.15 ์ˆ˜์ถ•๊ธฐ: 118 ์ด์™„๊ธฐ: 76 ๋งฅ๋ฐ•: 72 ์ •์ƒ ๋ฒ”์œ„ โœ“ 2024.01.15 ์•„์นจ: ๐Ÿ˜Š ์ข‹์Œ ์ ์‹ฌ: ๐Ÿ˜ ๋ณดํ†ต ์ €๋…: ๐Ÿ˜Š ์ข‹์Œ ํ‰๊ท  ๊ธฐ๋ถ„ ๐Ÿ˜Š

๐ŸŽจ 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. ๋ฐ์ดํ„ฐ ์ตœ์†Œํ™” - ํ•„์š” ์ด์ƒ์œผ๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ๋ณด๊ด€ํ•˜์ง€ ์•Š๊ธฐ

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. ์ž๋™ ๋ฐ์ดํ„ฐ ์ƒ์„ฑ
์ฝ”๋“œ๋กœ ํ…Œ์ŠคํŠธ ๋ฐ์ดํ„ฐ๋ฅผ ์ž๋™ ์ƒ์„ฑํ•˜๋ฉด ๋” ํŽธํ•ด:

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์ž์—๊ฒŒ ํŒ๋งคํ•˜๊ฑฐ๋‚˜ ๊ณต์œ ํ•˜๋Š” ๊ฒƒ๋„ ๊ธˆ์ง€.

โœ… ์˜ํ•™์  ์ฃผ์žฅ ๊ฒ€์ฆ
"์ด ์•ฑ์œผ๋กœ ๋‹น๋‡จ๋ณ‘์„ ์น˜๋ฃŒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค" ๊ฐ™์€ ๊ฒ€์ฆ๋˜์ง€ ์•Š์€ ์˜ํ•™์  ์ฃผ์žฅ ๊ธˆ์ง€.

์•ฑ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ์ž‘์„ฑ

์•ฑ์Šคํ† ์–ด ์„ค๋ช…์„ ์ž‘์„ฑํ•  ๋•Œ ์ฃผ์˜ํ•  ์ ๋“ค:

โŒ ํ”ผํ•ด์•ผ ํ•  ํ‘œํ˜„
โ€ข "์งˆ๋ณ‘ ์น˜๋ฃŒ", "์ง„๋‹จ", "์น˜์œ "
โ€ข "์˜์‚ฌ ๋Œ€์ฒด", "๋ณ‘์› ๋ฐฉ๋ฌธ ๋ถˆํ•„์š”"
โ€ข "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