Secret-Of-SwiftUI / SSDC22

๐Ÿคก ์‘~ ์งˆ๋ ค? deprecated ํ•˜๋ฉด ๊ทธ๋งŒ์ด์•ผ~
9 stars 0 forks source link

Build a research and care app, part 1: Setup onboarding #3

Open hyun99999 opened 2 years ago

hyun99999 commented 2 years ago

Learn how ResearchKit and CareKit can work together to take the tedium out of paper surveys. Code along with us and build a working care pilot app. Explore best practices for onboarding and consent with ResearchKit, and find out how your app can require participants to sign consent documents by leveraging CareKit's store and UI components. This is the first session in a three-part Code-Along series. To get the most out of this series, we recommend that you have some basic familiarity with both ResearchKit and CareKit. For more background, watch "ResearchKit and CareKit Reimaginedโ€ from WWDC19.

hyun99999 commented 2 years ago

WWDC21) Build a research and care app, part 1: Setup onboarding

Build a research and care app, part 1: Setup onboarding - WWDC21 - Videos - Apple Developer

*๋ณธ ๊ธ€์€ WWDC ๋ฅผ ๋ณด๊ณ , ๋ฒˆ์—ญ ๋ฐ ์š”์•ฝ ๊ทธ๋ฆฌ๊ณ  ์‹คํ–‰ํ•ด๋ณด๋Š” ์Šคํ„ฐ๋”” ํ”„๋กœ์ ํŠธ์˜ ์ผํ™˜์ž…๋‹ˆ๋‹ค.

๋“ค์–ด๊ฐ€๊ธฐ์ „์—

ResearchKit ๊ณผ CareKit ์— ๋Œ€ํ•ด์„œ ๋” ๋งŽ์€ ์ •๋ณด๋ฅผ ์–ป๊ณ  ์‹ถ๋‹ค๋ฉด ์•„๋ž˜์˜ ์†Œ๊ฐœ๊ธ€๋„ ๋„์›€์ด ๋  ๊ฒƒ์ž…๋‹ˆ๋‹ค.

ResearchKit๊ณผ CareKit

WWDC ๋‚ด์šฉ

แ„‰แ…ณแ„แ…ณแ„…แ…ตแ†ซแ„‰แ…ฃแ†บ 2022-06-07 แ„‹แ…ฉแ„’แ…ฎ 9 27 53

ResearchKit ์€ Apple ์ด GitHub ๋กœ ์œ ์ง€ํ•˜๋Š” ์˜คํ”ˆ์†Œ์Šค iOS framework ์ž…๋‹ˆ๋‹ค. ์ฐธ๊ฐ€์ž๊ฐ€ ์‹ ์ฒด์  ๋˜๋Š” ์ธ์ง€์  ์šด๋™๊ณผ ๊ฐ™์€ ํ™œ๋™์„ ์ˆ˜ํ–‰ํ•˜๋Š” ๋™์•ˆ ๋™์˜ ์ˆ˜์ง‘, ์„ค๋ฌธ ์กฐ์‚ฌ ๊ด€๋ฆฌ ๋ฐ ์„ผ์„œ ๋ฐ์ดํ„ฐ ๊ธฐ๋ก์„ ์œ„ํ•œ UI ์š”์†Œ๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

๋งˆ์ฐฌ๊ฐ€์ง€๋กœ CareKit ๋„ ์˜คํ”ˆ์†Œ์Šค ํ”„๋ ˆ์ž„์›Œํฌ์ด๋ฉฐ, ํ™˜์ž๋ฅผ ์ƒ๋Œ€ํ•˜๋Š” ์น˜๋ฃŒ ์•ฑ์„ ๊ตฌ์ถ•ํ•˜๋Š” ๋ฐ ๋„์›€์ด ๋˜๋„๋ก ์„ค๊ณ„๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ์ด ๊ธฐ๋Šฅ์€ ์‚ฌ์šฉ์ž๊ฐ€ ์›ํ•˜๋Š” ์ž‘์—…์„ ์˜ˆ์•ฝํ•˜๋Š” ๋ฐ ํƒ์›”ํ•ฉ๋‹ˆ๋‹ค. ์•ฝ์„ ๋ณต์šฉํ•˜๊ฑฐ๋‚˜ ์˜์‚ฌ๋ฅผ ๋ถ€๋ฅด๋Š” ๋“ฑ ์›ํ•˜๋Š” ์ž‘์—…์„ ์˜ˆ์•ฝํ•˜๋Š”๋ฐ ํƒ์›”ํ•ฉ๋‹ˆ๋‹ค. ๋˜ํ•œ ๊ฑด๊ฐ• ๊ด€๋ จ ๋ฐ์ดํ„ฐ๋ฅผ ์œ„ํ•œ ์•ˆ์ „ํ•œ ์ง€์†์„ฑ ๋ ˆ์ด์–ด๋ฅผ ์ œ๊ณตํ•˜๊ณ  ์ฐจํŠธ ์ž‘์„ฑ ์œ ํ‹ธ๋ฆฌํ‹ฐ๋ฅผ ๊ฐ–์ถ”๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

๊ณผ๊ฑฐ WWDC ์„ธ์…˜์—์„œ๋Š” ์ด ๋‘ ๊ฐ€์ง€ ํ˜•์ œ ํ”„๋ ˆ์ž„์›Œํฌ์— ๋Œ€ํ•ด ์ž์ฃผ ๋…ผ์˜ํ–ˆ์ง€๋งŒ ์˜ฌํ•ด๋Š” ์ด ๋‘ ๊ฐ€์ง€ ํ”„๋ ˆ์ž„์›Œํฌ๋ฅผ ํ•จ๊ป˜ ์‚ฌ์šฉํ•˜์—ฌ ํ›จ์”ฌ ๋” ๋‚˜์€ ํ™˜๊ฒฝ์„ ๋งŒ๋“ค ์ˆ˜ ์žˆ๋Š” ๋ฐฉ๋ฒ•์„ ๋ณด์—ฌ๋“œ๋ฆด ๊ฒƒ์ž…๋‹ˆ๋‹ค.

โœ๏ธ WWDC ๋ฅผ ์ง„ํ–‰ํ•˜๋‹ค๊ฐ€ ๊ฐ‘์ž๊ธฐ ํŽ˜์ด์Šคํƒ€์ž„์ด ์™€์„œโ€ฆ ๋ฐ›๋”๋ผ๊ตฌ์š”โ€ฆ? ์ค‘๊ฐ„์ค‘๊ฐ„ ์ง€๋ฃจํ•˜์ง€ ์•Š์€ ์—ฐ์ถœ๊ณผ ํฅ๋ฏธ๋กœ์šด ์ƒํ™ฉ์„ ๋งŒ๋“œ๋Š” ๋ชจ์Šต์ด ๋ณด์—ฌ์„œ ๋ฒˆ์—ญํ•ด๋ณด์•˜์Šต๋‹ˆ๋‹ค. ๋‹ค์Œ์€ ์ž๋ง‰ ๋ฒˆ์—ญ์˜ ์ „๋ฌธ์ž…๋‹ˆ๋‹ค.

๐Ÿคฆโ€โ™‚๏ธย Erik: ํ™˜์ž๋ฅผ ์œ„ํ•œ ๊ฒฝํ—˜... ์ฃ„์†กํ•ฉ๋‹ˆ๋‹ค. ๊ทธ๋ƒฅ ๋ฌด์Œ์œผ๋กœ ํ•  ๊ฑธ ๊ทธ๋žฌ๋‚˜? ์ž๋ฅด๊ณ  ๊ฐˆ๊นŒ? Crew: ์•„๋‹ˆ, ๊ทธ๋ƒฅ ๋ฐ›์•„.

๐Ÿคฆโ€โ™‚๏ธ Erik: ์•ผ, ์ œ์ด๋ฏธ. ๋ฌด์Šจ ์ผ์ด์•ผ?

๐Ÿคฆ๐Ÿปโ€โ™‚๏ธย Jamie: ์•ˆ๋…•, ์—๋ฆญ. ๋„ค๊ฐ€ ์ง€๊ธˆ WWDC๋ฅผ ์ค€๋น„ํ•˜๋Š๋ผ ๋ฐ”์˜๋‹ค๋Š” ๊ฑด ์•Œ์ง€๋งŒ, ์šฐ๋ฆฌ ๋ฌผ๋ฆฌ์น˜๋ฃŒ ์—ฐ๊ตฌ ์•ฑ์— ์ž๊ธˆ์ด ๋“ค์–ด์™”๋‹ค๋Š” ๊ฑธ ๋งํ•ด์ฃผ๊ณ  ์‹ถ์—ˆ์–ด! ์ด ์ตœ์ฒจ๋‹จ ์—ฐ๊ตฌ๊ฐ€ ์ •๋ง ๋งŽ์€ ์‚ฌ๋žŒ๋“ค์—๊ฒŒ ๋„์›€์ด ๋  ๊ฑฐ์•ผ! ๊ทธ๋ž˜์„œ, ์šฐ๋ฆฌ๊ฐ€ ๊ทธ ์•ฑ์˜ ์ฒซ ๋ฒˆ์งธ rev(=revision)์„ ๋‹น์žฅ ๋‚ด๋†“์„ ์ˆ˜ ์žˆ์„ ๊ฒƒ ๊ฐ™์•„?

๐Ÿคฆโ€โ™‚๏ธย Erik: ํ—ค์ด, ์ œ์ด๋ฏธ, ๊ทธ๊ฑฐ ์ •๋ง ๋Œ€๋‹จํ•œ ์†Œ์‹์ด์•ผ. ๊ทธ๋ฆฌ๊ณ  ๋‚œ ์ •๋ง ํฅ๋ถ„๋ผ. ์ค‘์š”ํ•œ ๊ฑด ์šฐ๋ฆฐ ์ง€๊ธˆ Dub Dub์˜ ์ฝ”๋“œ ์ง„ํ–‰์ค‘์ด๋ผ๋Š” ๊ฑฐ์—์š”.

๐Ÿคฆ๐Ÿปโ€โ™‚๏ธย Jamie: ์•„, ๊ทธ๋Ÿผ ๋ฒŒ์จ ์ฝ”๋”ฉ์„ ํ•˜์‹œ๋Š” ๊ฑด๊ฐ€์š”? ์™„๋ฒฝํ•˜๋„ค์š”! ์ด ์•ฑ์€ Recover ๋ผ๊ณ  ๋ถˆ๋ฆด ๊ฒ๋‹ˆ๋‹ค. ์ˆ˜์ˆ  ํ›„ ์ฐธ๊ฐ€์ž๋“ค์ด ๋ฌด๋ฆŽ์„ ํŠผํŠผํžˆ ํ•  ์ˆ˜ ์žˆ๋„๋ก ๋„์™€์ฃผ๋Š” ๋ฌผ๋ฆฌ์น˜๋ฃŒ ์•ฑ์ด ๋  ๊ฒƒ์ž…๋‹ˆ๋‹ค. ๊ทธ๋ฆฌ๊ณ  game changer๊ฐ€ ๋  ๊ฒƒ์ž…๋‹ˆ๋‹ค! ์ €๋Š” ๋งŽ์€ ํ›Œ๋ฅญํ•œ ์•„์ด๋””์–ด๋ฅผ ๊ฐ€์ง€๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ, ์šฐ์„ , ์ฐธ๊ฐ€์ž๋“ค์ด ์•ฑ์„ ์‚ฌ์šฉํ•˜๊ธฐ ์ „์— ์—ฐ๊ตฌ์— ์ฐธ์—ฌํ•˜๋Š” ๊ฒƒ์— ๋™์˜ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค ๋ชจ๋“  ๋ฒ•๋ฅ  ์šฉ์–ด์— ๋Œ€ํ•ด ์„ค๋ช…ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ๊ฒ์„ ์ฃผ์ง€ ์•Š๋Š” ๋ฐฉ๋ฒ•์œผ๋กœ ํ•˜๊ณ  ์‹ถ์Šต๋‹ˆ๋‹ค, you know? ์ฐธ๊ฐ€์ž๋“ค์˜ ์„œ๋ช…์„ ๋ฐ›์•„ ๋ฐ์ดํ„ฐ ๊ณต์œ ์— ๋™์˜ํ•˜๋„๋ก ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. Uh, one sec, Erik, ์ž„์ƒํŒ€์œผ๋กœ๋ถ€ํ„ฐ ๋ฉ”์‹œ์ง€๋ฅผ ๋ฐ›๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ์ฒซ ๋ฒˆ์งธ ์ฐธ๊ฐ€์ž๋“ค์€ ์›”์š”์ผ์— ์ˆ˜์ˆ  ํ›„(post-op) ํ‡ด์›ํ•  ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค. Erik, ์ผ๋‹จ onboarding ์„ ์‹œ์ž‘ํ•  ์ˆ˜ ์žˆ์„๊นŒ์š”? ๋‚˜๋จธ์ง€ ํŒ€์›๋“ค์„ ๋”ฐ๋ผ์žก์•„์•ผ ํ•˜๋Š”๋ฐ, ์ž ์‹œ ํ›„์— ๋‹ค์‹œ ์ „ํ™” ๋“œ๋ฆฌ๊ฒ ์Šต๋‹ˆ๋‹ค. ์ •๋ง ์ข‹์„ ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค.

๐Ÿคฆโ€โ™‚๏ธErik: OK. Uhโ€ฆ Alrightly then! ์ด๊ฒƒ์ด ์šฐ๋ฆฌ์˜ ์ฝ”๋“œ์ผ ์ผ ๊ฒƒ ๊ฐ™๋„ค์š”!

Recover ์ด๋ผ๋Š” ์•ฑ์˜ onboarding ์— ๋Œ€ํ•ด์„œ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•ด๋ด…์‹œ๋‹ค.

Onboarding and consent

์ฐธ๊ฐ€์ž์—๊ฒŒ ์šฐ๋ฆฌ๊ฐ€ ์ˆ˜์ง‘ํ•  ๋ฐ์ดํ„ฐ, ์•ก์„ธ์Šคํ•  ์ˆ˜ ์žˆ๋Š” ์‚ฌ๋žŒ, ๋ฌด์—‡์„ ์œ„ํ•ด ์‚ฌ์šฉ๋  ๊ฑด์ง€, ๋ณด๊ด€ ๊ธฐ๊ฐ„์„ ์„ค๋ช…ํ•˜๊ณ  ์ดํ•ดํ•˜๊ธฐ ์‰ฝ๊ฒŒ ๋งŒ๋“œ๋Š” ๊ฒƒ์ด ๊ฐ€์žฅ ์ค‘์š”ํ•ฉ๋‹ˆ๋‹ค.

์•ฑ์—์„œ ์ด ์ค‘์š”ํ•œ ๋ถ€๋ถ„์„ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์ˆ˜ํ–‰ํ•˜๋Š”๋ฐ ๋„์›€์ด ๋˜๋Š” ReserachKit ์˜ ๋ช‡ ๊ฐ€์ง€ ์ƒˆ๋กœ์šด ๋ชจ๋ฒ” ์‚ฌ๋ก€๋ฅผ ๋ณด์—ฌ๋“œ๋ฆฌ๊ฒ ์Šต๋‹ˆ๋‹ค. ๊ทธ๋ฆฌ๊ณ  ๋ณด๋„ˆ์Šค๋กœ ์˜จ๋ณด๋”ฉ ๋ฐ ๋™์˜ flow ๋ฅผ CareKit ๊ธฐ๋ฐ˜ ์•ฑ์— ์ ์šฉํ•˜๋Š” ์˜๋ฆฌํ•œ ๋ฐฉ๋ฒ•์„ ๋ณด์—ฌ๋“œ๋ฆฌ๊ฒ ์Šต๋‹ˆ๋‹ค.

3 8

์‹ค์ œ ๋™์˜ ๋ถ€๋ถ„์œผ๋กœ ๋„˜์–ด๊ฐ€๊ธฐ ์ „์— ์ด์™€ ๊ฐ™์ด ์ฐธ๊ฐ€์ž๋ฅผ ์•ˆ๋‚ดํ•˜๋Š” ๊ฒƒ์ด ๊ฒฝํ—˜์„ ๊ฐœ์„ ํ•˜๋Š” ๋ฐ ์‹ค์ œ๋กœ ๋„์›€์ด ๋œ๋‹ค๋Š” ๊ฒƒ์„ ์•Œ๊ฒŒ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

Recover

session resources ์— ์žˆ๋Š” starter project ๋ฅผ ํ†ตํ•ด์„œ ์ง„ํ–‰ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

https://github.com/carekit-apple/WWDC21-RecoverApp

(ํ•ด๋‹น ๊นƒํ—ˆ๋ธŒ ๋ ˆํฌ์ง€ํ† ๋ฆฌ์— Setup ๋ฐฉ๋ฒ•์ด ์žˆ์Šต๋‹ˆ๋‹ค.)

Setup

  1. Download the repository and the required submodules:
git clone --recurse-submodule https://github.com/carekit-apple/WWDC21-RecoverApp.git
  1. Choose the target namedย Recoverย and run the app.

(Recover Part1 ์˜ Recover ์„ ์„ ํƒํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค.)

CareKit

CareKit ์€ ๋ฏธ๋ฆฌ SPM ์„ ํ†ตํ•ด์„œ ์„ธํŒ…๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

https://github.com/carekit-apple/CareKit

Get Started!

4๊ฐœ์˜ ํŒŒ์ผ์„ ์ด๋ฒˆ ์„ธ์…˜์—์„œ ๋‹ค๋ฃฐ ๊ฒƒ์ž…๋‹ˆ๋‹ค. ์ฒซ๋ฒˆ์งธ๋กœ AppDelegate ์ž…๋‹ˆ๋‹ค. ์—ฌ๊ธฐ์—์„œ CareKit store ์™€ ์ƒํ˜ธ์ž‘์šฉํ•  ๊ฒƒ์ž…๋‹ˆ๋‹ค.

Files

Starter APP

starter app ์„ ์‹คํ–‰ํ•˜๊ฒŒ ๋˜๋ฉด ์ง€๊ธˆ์€ ์‚ฌ์‹ค์ƒ ์•„๋ฌด๊ฒƒ๋„ ์—†์Šต๋‹ˆ๋‹ค. ์ปจํ…์ธ ๊ฐ€ ์—†๋Š” OCKDailyPageViewController ๊ฐ€ ์žˆ์„๋ฟ์ž…๋‹ˆ๋‹ค. ํ•˜๋‹จ์— Insights ํƒญ๋„ ์žˆ์ง€๋งŒ ๋น„์–ด์žˆ์Šต๋‹ˆ๋‹ค. part 3 ๋™์•ˆ ์ด๊ฒƒ๋“ค์„ ์ฑ„์šธ ๊ฒƒ์ž…๋‹ˆ๋‹ค.

6

์ด์ œ ์ฐธ๊ฐ€์ž์˜ feed ์— consent card ๊ฐ€ ๋‚˜ํƒ€๋‚˜๋„๋ก ํ•ด๋ด…์‹œ๋‹ค. ์ด consent card ๋Š” ๋‚˜๋จธ์ง€ ์•ฑ์— ๋Œ€ํ•œ ์•ก์„ธ์Šค๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. ์™„๋ฃŒ๋˜๊ธฐ ์ „๊นŒ์ง€ ๋‹ค๋ฅธ ์ž‘์—…์ด ํ‘œ์‹œ๋˜์ง€ ์•Š์œผ๋ฉฐ, ์™„๋ฃŒ๋œ ํ›„์—๋Š” ๋‹ค๋ฅธ ๋ชจ๋“  ์ž‘์—…์ด ํ‘œ์‹œ๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

AppDelegate

import CareKit
import CareKitStore
import UIKit
import os.log

@main
class AppDelegate: UIResponder, UIApplicationDelegate {

    let storeManager = OCKSynchronizedStoreManager(
        wrapping: OCKStore(
            name: "com.apple.wwdc.carekitstore",
            type: .inMemory
        )
    )

    func application(
        _ application: UIApplication,
        didFinishLaunchingWithOptions
            launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {

        seedTasks()

        return true
    }

    // MARK: UISceneSession Life Cycle

    func application(
        _ application: UIApplication,
        configurationForConnecting connectingSceneSession: UISceneSession,
        options: UIScene.ConnectionOptions) -> UISceneConfiguration {

        UISceneConfiguration(
            name: "Default Configuration",
            sessionRole: connectingSceneSession.role
        )
    }

    // MARK: Seeding the Store

    private func seedTasks() {

        // 1.1 Persist an onboarding task

        // ์–ผ๋งˆ๋‚˜ ์ž์ฃผ ํ‘œ์‹œ๋˜๋Š”์ง€ ์ง€์ •ํ•˜๋Š” schedule ์„ ์ •์˜ํ•ด์•ผํ•ฉ๋‹ˆ๋‹ค.
        // ์˜จ๋ณด๋”ฉ์„ ์œ„ํ•ด ์ฐธ๊ฐ€์ž๊ฐ€ ๋™์˜๋ฅผ ํ• ๋•Œ๊นŒ์ง€ ๋งค์ผ๋งค์ผ schedule ์‚ฌ์šฉ.
        let onboardSchedule = OCKSchedule.dailyAtTime(
            hour: 0, minutes: 0,
            start: Date(), end: nil,
            text: "Task Due!",
            duration: .allDay)

        // ๋ฐฉ๊ธˆ ๋งŒ๋“  schedule ์„ ์ผ์ •์„ ์ „๋‹ฌํ•ด์„œ task ๋ฅผ ์ •์˜ํ•ด์•ผํ•ฉ๋‹ˆ๋‹ค.
        // id ๋Š” ์›ํ•˜๋Š” ๋Œ€๋กœ ์ง€์ •ํ•˜๊ณ , ๊ณ ์œ ํ•˜๋ฉด๋œ๋‹ค. ํ˜„์žฌ๋Š” ๋‹ค๋ฅธ ํŒŒ์ผ์—์„œ ์„ ์–ธ๋œ ์ƒ์ˆ˜๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
        var onboardTask = OCKTask(
            id: TaskIDs.onboarding,
            title: "Onboard",
            carePlanUUID: nil,
            schedule: onboardSchedule)

        // instructions ๋ฅผ ์ง€์ •ํ•˜๊ณ , ์˜จ๋ณด๋”ฉ์„ ๊ณ ์ˆ˜ํ•˜๋Š” ๊ฒƒ์ด ์˜ํ–ฅ๋ฐ›์ง€ ์•Š์•„์•ผ ํ•œ๋‹ค๋Š” ๊ฒƒ์„ ๋‚˜ํƒ€๋‚ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
        /// Instructions about how this task should be performed.
        onboardTask.instructions = "You'll need to agree to some terms and conditions before we get started!"
        /// If true, completion of this task will be factored into the patientโ€™s overall adherence. True by default.
        onboardTask.impactsAdherence = false
        // ์ด๊ฒƒ์€ ๊ธฐ๋ณธ์ ์œผ๋กœ ๋Œ€๋ถ€๋ถ„์˜ CareKit ๋งจ ์œ„์— ์™„๋ฃŒ๋ง์„ ์ฑ„์šฐ๋Š” ๊ฒƒ์œผ๋กœ ๊ฐ„์ฃผ๋˜์ง€ ์•Š๋Š” ๋‹ค๋Š” ๊ฒƒ์„ ์˜๋ฏธํ•œ๋‹ค.

        // store ์— task ๋ฅผ ์œ ์ง€ํ•  ์ˆ˜ ์žˆ๋‹ค.
        storeManager.store.addAnyTasks([onboardTask],
                                       callbackQueue: .main) { result in
            switch result {
            case let .success(tasks):
                Logger.store.info("Seeded \(tasks.count) tasks")
            case let .failure(error):
                Logger.store.warning("Failed to seed tasks:\(error as NSError)")
            }
        }
    }
}

์ด์ œ ์˜จ๋ณด๋”ฉ ์ž‘์—…์„ store ์— ์ค€๋น„ํ–ˆ์Šต๋‹ˆ๋‹ค. ์ฐธ๊ฐ€์ž์˜ ํ”ผ๋“œ์— ๋ณด์—ฌ์ค„ ์ค€๋น„๊ฐ€ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

CareViewController

import CareKit
import CareKitStore
import CareKitUI
import ResearchKit
import UIKit
import os.log

final class CareFeedViewController: OCKDailyPageViewController,
                                    OCKSurveyTaskViewControllerDelegate {

    /// ์‚ฌ์šฉ์ž๊ฐ€ ์ƒˆ ๋‚ ์งœ๋กœ ์Šค์™€์ดํ”„ํ•  ๋•Œ๋งˆ๋‹ค ํ˜ธ์ถœ๋ฉ๋‹ˆ๋‹ค.
    /// ๋‚ ์งœ๋ฅผ ๊ฒ€์‚ฌํ•˜๊ณ , ํ•ด๋‹น ๋‚ ์งœ์— ํ‘œ์‹œํ•  ๋‚ด์šฉ์„ ๊ฒฐ์ •ํ•œ ๋‹ค์Œ, ์ ์ ˆํ•œ ๋‚ด์šฉ์„ listViewController์— ์ถ”๊ฐ€ํ•˜๋Š” ๊ฒƒ์ด ์ผ์ž…๋‹ˆ๋‹ค.
    override func dailyPageViewController(
        _ dailyPageViewController: OCKDailyPageViewController,
        prepare listViewController: OCKListViewController,
        for date: Date) {

        // ๋จผ์ €, ์ฐธ๊ฐ€์ž๊ฐ€ ์˜จ๋ณด๋”ฉ์„ ์™„๋ฃŒํ–ˆ๋Š”์ง€ ํ™•์ธํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
        // 1.3 Check if onboarding is complete.
            checkIfOnboardingComlete { isOnboarding in

                // 1.5 If isn't, show an onboarding card.
                guard isOnboarding else {

                }
            }
    }

    // 1.2 Define a method that checks if onboarding is complete
    /// ๋ฉ”์„œ๋“œ ์•ˆ์— OCKOutomeQuery๋ฅผ ๋งŒ๋“ค๊ณ  ์˜จ๋ณด๋”ฉ ์ž‘์—…๊ณผ ๊ด€๋ จ๋œ ๋ชจ๋“  ๊ฒฐ๊ณผ๋ฅผ ์ฟผ๋ฆฌํ•ฉ๋‹ˆ๋‹ค.
    private func checkIfOnboardingComlete(_ completion: @escaping (Bool) -> Void) {
        var query = OCKOutcomeQuery()
        query.taskIDs = [TaskIDs.onboarding]

        // query ๋ฅผ ๊ฐ€์ง€๊ณ  ๊ฒฐ๊ณผ๊ฐ€ ๋ฐ˜ํ™˜๋˜์—ˆ๋Š”์ง€ ์—ฌ๋ถ€๋ฅผ ํ™•์ธํ•œ๋‹ค.
        storeManager.store.fetchAnyOutcomes(
            query: query,
            callbackQueue: .main) { result in
                switch result {
                case .failure:
                    Logger.feed.error("Failed to fetch onboarding outcomes!")
                    completion(false)

                // ์—†๋‹ค๋ฉด ์˜จ๋ณด๋”ฉ์ด ์•„๋‹‰ ์™„๋ฃŒ๋˜์ง€ ์•Š์€ ๊ฒƒ.
                case let .success(outcomes):
                    completion(!outcomes.isEmpty)
                }
            }
    }

    // 1.6 Refresh the content when onboarding completes
}

์ง€๊ธˆ๊นŒ์ง€์˜ ๋‹จ๊ณ„์ž…๋‹ˆ๋‹ค.

Survey

import CareKitStore
import ResearchKit

struct Surveys {

    private init() {}

    // MARK: Onboarding

    // 1.4 Construct an ORKTask for onboarding
    static func onboardingSurvey() -> ORKTask {

        // 1.4.1 The Welcome Instruction step.
        let welcomeInstructionStep = ORKInstructionStep(
            identifier: "onboarding.welcome"
        )

        welcomeInstructionStep.title = "Welcome!"
        welcomeInstructionStep.detailText = "Thank you for joining our study. Tap Next to learn more before signing up."
        welcomeInstructionStep.image = UIImage(named: "welcome-image")
        welcomeInstructionStep.imageContentMode = .scaleAspectFill

        // 1.4.2 The Informed Consent Instruction step.
        // ...

        // 1.4.3 The Signature step (using WebView).
        // ...

        // 1.4.4 The Request Permissions step.
        // ...

        // 1.4.5 Completion Step
        // ...

    }
}

1.4.1

7

1.4.2

3 8
// 1.4.2 The Informed Consent Instruction step.
        let studyOverviewInstructionStep = ORKInstructionStep(
            identifier: "onboarding.overview"
        )

        studyOverviewInstructionStep.title = "Before You Join"
        studyOverviewInstructionStep.iconImage = UIImage(systemName: "checkmark.seal.fill")

        // ๋ณธ๋ฌธ ํ•ญ๋ชฉ ๋งŒ๋“ค๊ธฐ.
        // ๊ธ€๋จธ๋ฆฌ ๋Œ€์‹  ์ด๋ฏธ์ง€๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
        let heartBodyItem = ORKBodyItem(
            text: "The study will ask you to share some of your health data.",
            detailText: nil,
            image: UIImage(systemName: "heart.fill"),
            learnMoreItem: nil,
            bodyItemStyle: .image
        )

        let completeTasksBodyItem = ORKBodyItem(
            text: "You will be asked to complete various tasks over the duration of the study.",
            detailText: nil,
            image: UIImage(systemName: "checkmark.circle.fill"),
            learnMoreItem: nil,
            bodyItemStyle: .image
        )

        let signatureBodyItem = ORKBodyItem(
            text: "Before joining, we will ask you to sign an informed consent document.",
            detailText: nil,
            image: UIImage(systemName: "signature"),
            learnMoreItem: nil,
            bodyItemStyle: .image
        )

        let secureDataBodyItem = ORKBodyItem(
            text: "Your data is kept private and secure.",
            detailText: nil,
            image: UIImage(systemName: "lock.fill"),
            learnMoreItem: nil,
            bodyItemStyle: .image
        )

        studyOverviewInstructionStep.bodyItems = [
            heartBodyItem,
            completeTasksBodyItem,
            signatureBodyItem,
            secureDataBodyItem
        ]

1.4.3

4 9
// 1.4.3 The Signature step (using WebView).
        // ๋‹คํ–‰ํžˆ๋„ ResearchKit ์—์„œ ์ด ๋ถ€๋ถ„์„ ์‰ฝ๊ฒŒ ๊ตฌํ˜„ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.
        // signature ์€ ORKWebViewStep ์œผ๋กœ ์ˆ˜์ง‘ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
        // informedConsentHTML ์€ HTML ์–ธ์–ด๋กœ ์ž‘์„ฑ๋˜์—ˆ๊ณ  WebView ๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
        let webViewStep = ORKWebViewStep(
            identifier: "onboarding.signatureCapture",
            html: informedConsentHTML
        )

        // true ๋กœ ์„ค์ •ํ•˜๊ฒŒ๋˜๋ฉด, ResearchKit ์—์„œ signature box ๋ฅผ ํ‘œ์‹œํ•ฉ๋‹ˆ๋‹ค.
        webViewStep.showSignatureAfterContent = true

1.4.4

5 10
// 1.4.4 The Request Permissions step.
        // HealthKit ๊ถŒํ•œ์„ ์ˆ˜์ง‘ํ•ฉ๋‹ˆ๋‹ค. HealthKitPermissionType ์„ ๋‘๊ฐ€์ง€ ์ง€์ •ํ•ด์•ผํ•˜๋Š”๋ฐ ์“ธ ๊ถŒํ•œ๊ณผ ์ฝ์„ ๊ถŒํ•œ์ž…๋‹ˆ๋‹ค.
        // (wwdc20 ์—์„œ ์†Œ๊ฐœํ•œ ํƒ€์ž….)
        let healthKitTypesToWrite: Set<HKSampleType> = [
            HKObjectType.quantityType(forIdentifier: .bodyMassIndex)!,
            HKObjectType.quantityType(forIdentifier: .activeEnergyBurned)!,
            HKObjectType.workoutType()
        ]

        let healthKitTypesToRead: Set<HKObjectType> = [
            HKObjectType.characteristicType(forIdentifier: .dateOfBirth)!,
            HKObjectType.workoutType(),
            HKObjectType.quantityType(forIdentifier: .appleStandTime)!,
            HKObjectType.quantityType(forIdentifier: .appleExerciseTime)!
        ]

        let healthKitPermissionType = ORKHealthKitPermissionType(
            sampleTypesToWrite: healthKitTypesToWrite,
            objectTypesToRead: healthKitTypesToRead
        )

        // wwdc21 ์—์„œ๋Š” ์ถ”๊ฐ€์ ์œผ๋กœ ์ƒˆ๋กœ์šด ๋‘ ๊ฐ€์ง€ ๊ถŒํ•œ์„ ๋„์ž…ํ•ฉ๋‹ˆ๋‹ค.

        /// ์•Œ๋ฆผ ํ‘œ์‹œ, ์•ฑ ๋ฐฐ์ง€, ์‚ฌ์šด๋“œ ์žฌ์ƒ์— ๋Œ€ํ•œ ๊ถŒํ•œ์„ ์š”์ฒญํ•˜๋Š” type ์ž…๋‹ˆ๋‹ค.
        let notificationsPermissionType = ORKNotificationPermissionType(
            authorizationOptions: [.alert, .badge, .sound]
        )

        /// device motion data ์— ์•ก์„ธ์Šค๋ฅผ ์š”์ฒญํ•˜๋Š”๋ฐ ๋„์›€์ด๋˜๋Š” type ์ž…๋‹ˆ๋‹ค.
        let motionPermissionType = ORKMotionActivityPermissionType()

        // read and write, notification, activity ์•ก์„ธ์Šค ์š”์ฒญ.
        let requestPermissionsStep = ORKRequestPermissionsStep(
            identifier: "onboarding.requestPermissionsStep",
            permissionTypes: [
                healthKitPermissionType,
                notificationsPermissionType,
                motionPermissionType
            ]
        )

        requestPermissionsStep.title = "Health Data Request"
        requestPermissionsStep.text = "Please review the health data types below and enable sharing to contribute to the study."

1.4.5

11
// 1.4.5 Completion Step
        // Jamie ์˜ ์—ฐ๊ตฌ์— ์ฐธ์—ฌํ•ด ์ค€ ๊ฒƒ์— ๋Œ€ํ•ด ๊ฐ์‚ฌํ•˜๋Š” ๋‹จ๊ณ„์ž…๋‹ˆ๋‹ค.
        let completionStep = ORKCompletionStep(
            identifier: "onboarding.completionStep"
        )

        completionStep.title = "Enrollment Complete"
        completionStep.text = "Thank you for enrolling in this study. Your participation will contribute to meaningful research!"

        /// ์ง€๊ธˆ๊นŒ์ง€์˜ ORKCompletionStep ์„ ํ†ตํ•ด ORKOrderedTask ๋ฅผ ๋ฐ˜ํ™˜.
        let surveyTask = ORKOrderedTask(
            identifier: "onboard",
            steps: [
                welcomeInstructionStep,
                studyOverviewInstructionStep,
                webViewStep,
                requestPermissionsStep,
                completionStep
            ]
        )

        return surveyTask

CareFeedViewController ๋กœ ๋Œ์•„๊ฐ€์„œ CareKit ๊ธฐ๋ฐ˜์˜ ์•ฑ์—์„œ ์ด ๊ธฐ๋Šฅ์„ ํ‘œ์‹œํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ๋‹ค์‹œ ์‚ดํŽด๋ณด์ž!

CareFeedViewController

// MARK: SurveyTaskViewControllerDelegate

// 1.6 Refresh the content when onboarding completes
    //
    func surveyTask(
        viewController: OCKSurveyTaskViewController,
        for task: OCKAnyTask,
        didFinish result: Result<ORKTaskViewControllerFinishReason, Error>) {

        // ์ฐธ๊ฐ€์ž๊ฐ€ ์ค‘๊ฐ„์— ์ทจ์†Œํ•œ ๊ฒƒ์ด ์•„๋‹ˆ๋ผ ์˜จ๋ณด๋”ฉ์„ ๋๊นŒ์ง€ ์„ฑ๊ณต์‹œํ‚จ ๊ฒฝ์šฐ์ž…๋‹ˆ๋‹ค.
        // ์ด๋•Œ ํ”ผ๋“œ์˜ ์ƒˆ๋กœ๊ณ ์นจ ์ง„ํ–‰.
        if case let .success(reason) = result, reason == .completed {
            reload()
        }
    }

์ž, ์ด์ œ ์•ฑ์ด ์ œ๋Œ€๋กœ ์ž‘๋™ํ•˜๋Š”์ง€ ํ™•์ธํ•ด ๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

๐Ÿ’ก ๋‹จ, HealthKit ์„ ์‚ฌ์šฉํ•˜๊ธฐ ๋•Œ๋ฌธ์— ํ•ด๋‹น Bundle Identifier ๋ฅผ App Deleveloper ์—์„œ ๋“ฑ๋กํ•ด์ค˜์•ผ ํ•ฉ๋‹ˆ๋‹ค~!

์˜จ๋ณด๋”ฉ์ด ๋๋‚˜๊ฒŒ๋˜๋ฉด consent ๋ฅผ ์™„๋ฃŒํ•˜๋ผ๋Š” ๋ฉ”์‹œ์ง€๊ฐ€ ๋” ์ด์ƒ ํ‘œ์‹œ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๋Œ€์‹ , ์•ฑ์˜ ๋ชจ๋“  ๋‚ด์šฉ์ด ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค.

๋‹ค์Œ ์„ธ์…˜์—์„œ ์ด ๋‚ด์šฉ์„ ์‚ดํŽด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค!

โœ๏ธ Hey Siri, can you message Jamieย and tell him that weโ€™re done with the onboarding and consent?

ํฌโ€ฆ ๊ฐ„์ง€