Secret-Of-SwiftUI / SSDC22

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

Build a research and care app, part 2: Schedule tasks #6

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. Continue coding along with us and explore how you can make it easier than ever to schedule surveys for your study participants. You'll also learn advanced techniques for crafting evolving regiments in CareKit and see how ResearchKit's active tasks can help capture important measurements out of clinic. This is the second session in a three-part Code-Along series. To get the most out of this session, we recommend first watching โ€œBuild a research and care app, part 1.โ€ And for more background on these frameworks, watch "ResearchKit and CareKit Reimaginedโ€ from WWDC19.

hyun99999 commented 2 years ago

WWDC21) Build a research and care app, part 2: Schedule tasks

Build a research and care app, part 2: Schedule tasks - WWDC21 - Videos - Apple Developer

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

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

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

ResearchKit๊ณผ CareKit

part1 ์—์„œ๋Š” onboarding ๊ณผ consent ์— ๋Œ€ํ•ด์„œ ๋งˆ์ณค์Šต๋‹ˆ๋‹ค.

แ„‰แ…ณแ„แ…ณแ„…แ…ตแ†ซแ„‰แ…ฃแ†บ 2022-06-07 แ„‹แ…ฉแ„’แ…ฎ 10 34 21

๐Ÿคฆ๐Ÿปโ€โ™‚๏ธย Erick: oh, hang on. Jamie ๋กœ๋ถ€ํ„ฐ ๋ฉ”์‹œ์ง€๋ฅผ ๋ฐ›์€ ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค.

โ€œ์•ฑ์— ๋Œ€ํ•œ ์ƒˆ๋กœ์šด ์•„์ด๋””์–ด๋ฅผ ์–ป์—ˆ์–ด์š”.โ€

โ€œ๋‚ด ๋งˆ์ง€๋ง‰ text ๋ดค์–ด์š”?โ€

์™€ ๊ฐ™์ด ๋ฉ”์‹œ์ง€๋ฅผ ๋ฐ›๊ณ , mail ๊ณผ Notes ์˜ ์•Œ๋ฆผ์„ Erick ์ด ๋ฐ›๊ฒŒ๋ฉ๋‹ˆ๋‹ค.

๋ฐœํ‘œ๊ฐ€ ์ฐธ ๊ธฐ๊ฐ€ ๋ง‰ํžˆ๊ตฐ์š” ํฌ..

1

์ž! ๊ทธ๋Ÿผ ์ด๋ฒˆ์—๋Š” ๋ฌด์—‡์„ ํ•ด์•ผํ• ์ง€ ๋ด…์‹œ๋‹ค.

2

Display forms, persisting some data, dynamic schedules, range of motion..

3 4 5 6

daily check-in survey

*์•„๋ž˜์˜ part1 ์—์„œ ์ž‘์„ฑํ•œ ์ฝ”๋“œ๋ฅผ ๊ทธ๋Œ€๋กœ ์‚ฌ์šฉํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

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() {

        let onboardSchedule = OCKSchedule.dailyAtTime(
            hour: 0, minutes: 0,
            start: Date(), end: nil,
            text: "Task Due!",
            duration: .allDay
        )

        var onboardTask = OCKTask(
            id: TaskIDs.onboarding,
            title: "Onboard",
            carePlanUUID: nil,
            schedule: onboardSchedule
        )
        onboardTask.instructions = "You'll need to agree to some terms and conditions before we get started!"
        onboardTask.impactsAdherence = false

        // 2.1 Add a check-in task

        // 2.6 Add a range of motion 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)")
            }
        }
    }
}
// 2.1 Add a check-in task

        let checkInSchedule = OCKSchedule.dailyAtTime(
            hour: 8, minutes: 0,
            start: Date(), end: nil,
            text: nil
        )

        // ๐Ÿ”ฅ ๊ณ ์œ ํ•œ ์‹๋ณ„์ž์™€ ๋ฐฉ๊ธˆ ์ •์˜ํ•œ ์ผ์ •์œผ๋กœ check-in task ๋ฅผ ๋งŒ๋“ญ๋‹ˆ๋‹ค.
        let checkInTask = OCKTask(
            id: TaskIDs.checkIn,
            title: "Check In",
            carePlanUUID: nil,
            schedule: checkInSchedule
        )

        // 2.6 Add a range of motion task

        storeManager.store.addAnyTasks(
            // ๐Ÿ”ฅ ์œ ์ง€ํ•˜๊ธฐ์œ„ํ•ด์„œ ์—ฌ๊ธฐ์— task ๋ฅผ ๋„ฃ์Šต๋‹ˆ๋‹ค.
            [onboardTask, checkInTask],
            callbackQueue: .main) { result in

์˜จ๋ณด๋”ฉ ์ž‘์—…๊ณผ ๋งˆ์ฐฌ๊ฐ€์ง€๋กœ ๋‹ค์Œ ๋‹จ๊ณ„๋Š” CareFeedViewController ๋กœ ์ด๋™ํ•ด์„œ CareKit ์— task ๋ฅผ ํ‘œ์‹œํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์•Œ๋ ค์ฃผ๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.

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

final class CareFeedViewController: OCKDailyPageViewController,
                                    OCKSurveyTaskViewControllerDelegate {

    override func dailyPageViewController(
        _ dailyPageViewController: OCKDailyPageViewController,
        prepare listViewController: OCKListViewController,
        for date: Date) {

        checkIfOnboardingIsComplete { isOnboarded in

            guard isOnboarded else {

                let onboardCard = OCKSurveyTaskViewController(
                    taskID: TaskIDs.onboarding,
                    eventQuery: OCKEventQuery(for: date),
                    storeManager: self.storeManager,
                    survey: Surveys.onboardingSurvey(),
                    extractOutcome: { _ in [OCKOutcomeValue(Date())] }
                )

                onboardCard.surveyDelegate = self

                listViewController.appendViewController(
                    onboardCard,
                    animated: false
                )

                return
            }

            // 2.2 Query and display a card for each task.
        }
    }

    private func checkIfOnboardingIsComplete(_ completion: @escaping (Bool) -> Void) {

        var query = OCKOutcomeQuery()
        query.taskIDs = [TaskIDs.onboarding]

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

    // 2.3 Query all the tasks to be displayed on a given date

    // 2.4 Create a card for a given task

    // MARK: SurveyTaskViewControllerDelegate

    func surveyTask(
        viewController: OCKSurveyTaskViewController,
        for task: OCKAnyTask,
        didFinish result: Result<ORKTaskViewControllerFinishReason, Error>) {

        if case let .success(reason) = result, reason == .completed {
            reload()
        }
    }
}

์ด๋ฒˆ์—๋Š” solution ์„ ์ข€ ๋” generic ํ•˜๊ฒŒ ๋งŒ๋“ค์–ด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค. ํ˜„์žฌ ๋‚ ์งœ์˜ ๋ชจ๋“  task ๋ฅผ ๊ฐ€์ ธ์˜จ ๋‹ค์Œ ๊ฐ ์ž‘์—…์— ๋Œ€ํ•ด์„œ ๋ทฐ ์ปจํŠธ๋กค๋Ÿฌ๋ฅผ ์ƒ์„ฑํ•˜๊ณ , ํ•ด๋‹น ๋ทฐ ์ปจํŠธ๋กค๋Ÿฌ๋ฅผ list ์— ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค. task ๋ฅผ ์ถ”๊ฐ€ํ• ์ˆ˜๋ก ํฌ๊ธฐ๊ฐ€ ์กฐ์ •๋ฉ๋‹ˆ๋‹ค.

// 2.2 Query and display a card for each task.

            self.fetchTasks(on: date) { tasks in
                tasks.compactMap {
                    self.taskViewController(for: $0, on: date)
                }.forEach {
                    listViewController.appendViewController($0, animated: false)
                }
            }

// ...

// 2.3 Query all the tasks to be displayed on a given date

    private func fetchTasks(
        on date: Date,
        completion: @escaping([OCKAnyTask]) -> Void) {

        var query = OCKTaskQuery(for: date)
        // โœ… Determines if tasks with no events should be included in the query results or not. False be default.
        // ๐Ÿ”ฅ ์˜ˆ์•ฝ๋œ ์ด๋ฒคํŠธ๊ฐ€ ์—†๋Š” ์ž‘์—…์„ ์ œ์™ธํ•˜๋„๋ก ์ง€์ •ํ•ฉ๋‹ˆ๋‹ค.
        query.excludesTasksWithNoEvents = true

        storeManager.store.fetchAnyTasks(
            query: query,
            callbackQueue: .main) { result in

            switch result {

            case .failure:
                Logger.feed.error("Failed to fetch tasks for date \(date)")
                completion([])
            // ๐Ÿ”ฅ query ๊ฐ€ ๋ฐ˜ํ™˜๋˜๋ฉด ๊ฐ€์ ธ์˜จ tasks ๋ฅผ completion handler ๋กœ ์ „๋‹ฌํ•ฉ๋‹ˆ๋‹ค.
            case let .success(tasks):
                completion(tasks)
            }
        }
    }

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

query.excludesTasksWithNoEvents = true

๋˜ํ•œ, ์šฐ๋ฆฌ๋Š” task ๋ฅผ ์ˆ˜ํ–‰ํ•˜๊ณ  ๋ทฐ ์ปจํŠธ๋กค๋Ÿฌ๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋Š” ๋ฉ”์„œ๋“œ๋ฅผ ์ž‘์„ฑํ•ด์•ผํ•ฉ๋‹ˆ๋‹ค.

task ์˜ id ๋ฅผ ํ™•์ธํ•˜๊ณ , check-in task ์˜ ๊ฒฝ์šฐ๋Š” part1 ์—์„œ ์†Œ๊ฐœํ•œ ๊ฒƒ์ฒ˜๋Ÿผ SurveyTaskViewController ๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.

// 2.4 Create a card for a given task

    private func taskViewController(
        for task: OCKAnyTask,
        on date: Date) -> UIViewController? {

        switch task.id {

        case TaskIDs.checkIn:
            // ๐Ÿ”ฅ part1 ๊ณผ ๋งˆ์ฐฌ๊ฐ€์ง€๋กœ task, event query, store manager ์— ๋Œ€ํ•œ ์ฐธ์กฐ๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.
            let survey = OCKSurveyTaskViewController(
                task: task,
                eventQuery: OCKEventQuery(for: date),
                storeManager: storeManager,
                survey: Surveys.checkInSurvey(),
                extractOutcome: Surveys.extractAnswersFromCheckInSurvey
            )

            return survey

        case TaskIDs.rangeOfMotionCheck:
            let survey = OCKSurveyTaskViewController(
                task: task,
                eventQuery: OCKEventQuery(for: date),
                storeManager: storeManager,
                survey: Surveys.rangeOfMotionCheck(),
                extractOutcome: Surveys.extractRangeOfMotionOutcome
            )

            return survey

        default:
            return nil
        }
    }

ResearchKit ์˜ survey ์™€ ๊ฒฐ๊ณผ๋ฅผ CareKit ๊ฒฐ๊ณผ ๊ฐ’์œผ๋กœ ๋ณ€ํ™˜ํ•˜๋Š” ๊ธฐ๋Šฅ๋„ ๋งŒ๋“ค์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

๋จผ์ €, ResearchKit ๊ณผ CareKit ์— ๋Œ€ํ•ด ์‚ดํŽด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

7

์šฐ๋ฆฌ Recover ์•ฑ์ด ResearchKit survey ๋ฅผ ๋งŒ๋“ค ๊ฒƒ์ด๊ณ , ResearchKit ์€ survey flow ๋ฅผ ์ง„ํ–‰ํ•˜๋ฉด์„œ ์ฐธ๊ฐ€์ž๋ฅผ ์•ˆ๋‚ดํ•ฉ๋‹ˆ๋‹ค.

๊ทธ๋Ÿฌ๋ฉด ORKTaskResult ๊ฐ€ ์ƒ์„ฑ๋˜์–ด ์šฐ๋ฆฌ์˜ ์•ฑ์œผ๋กœ ๋ฐ˜ํ™˜๋ฉ๋‹ˆ๋‹ค.

๊ทธ๋ฆฌ๊ณ ๋‚˜์„œ ์šฐ๋ฆฌ์˜ ์•ฑ์€ ResearchKit ์˜ ๊ฒฐ๊ณผ๋ฅผ CareKit ์˜ ์Šคํ† ์–ด์— ์œ ์ง€๋˜๋„๋ก CareKit ๊ฒฐ๊ณผ ๊ฐ’์œผ๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค.

์ƒˆ๋กœ์šด ๊ฒฐ๊ณผ๋ฅผ ์ €์žฅํ•˜๋ฉด completion ring ์ด ์ฑ„์›Œ์ง€๊ณ , card UI ๊ฐ€ ์—…๋ฐ์ดํŠธ ๋ฉ๋‹ˆ๋‹ค.

Survey.swift ์—์„œ ๋ฉ”์„œ๋“œ๋ฅผ ์ •์˜ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

import CareKitStore
import ResearchKit

struct Surveys {

    private init() {}

    // MARK: Onboarding

    static func onboardingSurvey() -> ORKTask {

        // 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

        // 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
        ]

        // The Signature step (using WebView).
        let webViewStep = ORKWebViewStep(
            identifier: "onboarding.signatureCapture",
            html: informedConsentHTML
        )

        webViewStep.showSignatureAfterContent = true

        // The Request Permissions step.
        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
        )

        let notificationsPermissionType = ORKNotificationPermissionType(
            authorizationOptions: [.alert, .badge, .sound]
        )

        let motionPermissionType = ORKMotionActivityPermissionType()

        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."

        // Completion Step
        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!"

        let surveyTask = ORKOrderedTask(
            identifier: "onboard",
            steps: [
                welcomeInstructionStep,
                studyOverviewInstructionStep,
                webViewStep,
                requestPermissionsStep,
                completionStep
            ]
        )

        return surveyTask
    }

    // MARK: 2.5 Check In Survey

    // MARK: 2.7 Range of Motion
}

Create Survey Method

์šฐ๋ฆฌ๋Š” ์ฐธ๊ฐ€์ž๊ฐ€ ์žก์„ ์ž๋Š” ์‹œ๊ฐ„๊ณผ ๊ณ ํ†ต์„ ๋Š๋ผ๋Š” ์‹œ๊ฐ„ ์‚ฌ์ด์— ์—ฐ๊ด€์„ฑ์ด ์žˆ๋Š”์ง€ ์•Œ์•„๋ณด๋ ค๊ณ  ํ•ฉ๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ ๋‘๊ฐ€์ง€ ์งˆ๋ฌธ์„ ๋งŒ๋“ค๊ฒ ์Šต๋‹ˆ๋‹ค.

๋‘ item ์„ ๋‹จ์ผ ์–‘์‹์— ์ „๋‹ฌํ•œ ๋‹ค์Œ ORKOrderedTask ๋ฅผ ์ƒ์„ฑํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค.

// MARK: 2.5 Check In Survey

    static let checkInIdentifier = "checkin"
    static let checkInFormIdentifier = "checkin.form"
    static let checkInPainItemIdentifier = "checkin.form.pain"
    static let checkInSleepItemIdentifier = "checkin.form.sleep"

    // โœ… 2.5.1 create the survey
    static func checkInSurvey() -> ORKTask {

// ๊ณ ํ†ต

// ์ตœ๋Œ€ ํ†ต์ฆ์„ 10์œผ๋กœ ์ง€์ •ํ•˜๊ณ , ์ตœ์†Œ๊ฐ’์„ 1๋กœ ์ง€์ •ํ•˜๊ณ , ๋‹จ๊ณ„ ํฌ๊ธฐ๋ฅผ 1๋กœ ์„ค์ •ํ•˜์—ฌ ๋ฐ˜์˜ฌ๋ฆผ ์ˆซ์ž๋งŒ ํ—ˆ์šฉํ•˜๋„๋ก ์„ค์ •ํ•˜๊ณ , ์ตœ์†Œ/์ตœ๋Œ€์— ๋Œ€ํ•œ ์„ค๋ช…์„ ์ œ๊ณตํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.
        let painAnswerFormat = ORKAnswerFormat.scale(
            withMaximumValue: 10, // ์ตœ๋Œ€ ํ†ต์ฆ 10.
            minimumValue: 1,      // ์ตœ์†Œ๊ฐ’ 1.
            defaultValue: 0,
            step: 1,              // ๋‹จ๊ณ„ ํฌ๊ธฐ 1.
            vertical: false,      // ๋ฐ˜์˜ฌ๋ฆผ ์ˆซ์ž๋งŒ ํ—ˆ์šฉ.
            maximumValueDescription: "Very painful", // ์ตœ๋Œ“๊ฐ’ ์„ค๋ช….
            minimumValueDescription: "No pain"       // ์ตœ์†Ÿ๊ฐ’ ์„ค๋ช….
        )

        // ๐Ÿ”ฅ pain ์— ๋Œ€ํ•œ ์งˆ๋ฌธ
        let painItem = ORKFormItem(
            identifier: checkInPainItemIdentifier,
            text: "How would you rate your pain?",
            answerFormat: painAnswerFormat
        )
        painItem.isOptional = false

// ์ˆ˜๋ฉด
        // ๐Ÿ”ฅ ์ตœ์†Œ ์ˆ˜๋ฉด ์‹œ๊ฐ„ 0์‹œ๊ฐ„, ์ตœ๋Œ€ ์ˆ˜๋ฉด ์‹œ๊ฐ„ 12์‹œ๊ฐ„.
        let sleepAnswerFormat = ORKAnswerFormat.scale(
            withMaximumValue: 12,
            minimumValue: 0,
            defaultValue: 0,
            step: 1,
            vertical: false,
            maximumValueDescription: nil,
            minimumValueDescription: nil
        )

        let sleepItem = ORKFormItem(
            identifier: checkInSleepItemIdentifier,
            text: "How many hours of sleep did you get last night?",
            answerFormat: sleepAnswerFormat
        )
        sleepItem.isOptional = false

// formStep
        // ๐Ÿ”ฅ formStep ์—๋Š” ๊ณ ์œ ํ•œ ์‹๋ณ„์ž, ์ œ๋ชฉ ๋ฐ ํ…์ŠคํŠธ๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.
        let formStep = ORKFormStep(
            identifier: checkInFormIdentifier,
            title: "Check In",
            text: "Please answer the following questions."
        )

        // ๐Ÿ”ฅ form ์„ ๊ฑด๋„ˆ๋›ธ ์ˆ˜ ์—†๋„๋ก ํ•˜๋ ค๋ฉด ์ด ํ•ญ๋ชฉ์„ false ์„ค์ •ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
        formStep.isOptional = false
        // ๐Ÿ”ฅ ์œ„์˜ ๋‘ ํ•ญ๋ชฉ(painItem, sleepItem)์„ formStep ์— ์ „๋‹ฌ.
        formStep.formItems = [
            painItem,
            sleepItem
        ]

        // ๐Ÿ”ฅ item ์„ ๊ฐ€์ง€๊ณ  ORKOrderedTask ์ƒ์„ฑ.
        let surveyTask = ORKOrderedTask(
            identifier: checkInIdentifier,
            steps: [formStep]
        )

        return surveyTask
    }

Create CareKit values to persist.

๋‘ ๋ฒˆ์งธ ํ•จ์ˆ˜๋Š” ResearchKit task ๊ฒฐ๊ณผ๋ฅผ ๊ฐ€์ ธ์™€์„œ ์ง€์†ํ•  CareKit ๊ฐ’์„ ์ƒ์„ฑํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ๊ทธ ์ „์— ResearchKit task ์˜ ๊ตฌ์กฐ๋ฅผ ์‚ดํŽด๋ณด๊ณ  ์–ด๋–ป๊ฒŒ ๋ถ„์„ํ•˜๋Š”์ง€ ๋ฐฉ๋ฒ•์— ๋Œ€ํ•ด์„œ ์•Œ์•„๋ด…์‹œ๋‹ค.

ORKTaskResults ๋Š” ์ค‘์ฒฉ๋œ ํƒ€์ž…์ด๋ผ๋Š” ๊ฒƒ์„ ์ดํ•ดํ•˜๋Š” ๊ฒƒ์ด ์ค‘์š”ํ•ฉ๋‹ˆ๋‹ค. check-in survey ๋ฅผ ์œ„ํ•œ root ๊ฒฐ๊ณผ์—์„œ ์‹œ์ž‘ํ•œ ๋‹ค์Œ checkin.form ์„ dril down(์•„๋ž˜๋กœ ๋‚ด๋ ค๊ฐ€๋ฉฐ ๋ถ„์„) ํ•ฉ๋‹ˆ๋‹ค.

8

checkin.form ์€ ๋‘ ๊ฐ€์ง€ children ์„ ๊ฐ€์ง€๊ณ  ์žˆ๊ณ , ์ด๊ฒƒ์„ ํŒŒํ—ค์ณ์•ผ ํ•ฉ๋‹ˆ๋‹ค.

๋จผ์ €, pain item ๊ณผ sleep itme ์‹๋ณ„์ž์— ๋Œ€ํ•ด ์ฃผ์–ด์ง„ ๋‹ต์„ ์ฐพ๊ณ  ์‹ถ์Šต๋‹ˆ๋‹ค. ์˜ˆ์‹œ์—์„œ๋Š” 4์™€ 11 ์ž…๋‹ˆ๋‹ค.

9 10 11

์‹œ๊ฐ์ ์œผ๋กœ ์‚ดํŽด๋ดค๊ณ , ์ฝ”๋“œ์—์„œ๋„ ๊ฐ™์€ ํ”„๋กœ์„ธ์Šค์ž…๋‹ˆ๋‹ค.

// 2.5.2 Parse the results

static func extractAnswersFromCheckInSurvey(
        _ result: ORKTaskResult) -> [OCKOutcomeValue]? {

        guard
            let response = result.results?
                .compactMap({ $0 as? ORKStepResult })
                // ๐Ÿ”ฅ 'checkin.form' ์‹๋ณ„์ž๋งŒ ์„ ํƒ
                .first(where: { $0.identifier == checkInFormIdentifier }),

            // ๐Ÿ”ฅ ORKScaleQuestionResult ํƒ€์ž…์„ ๊ฐ€์ง„ ๋ชจ๋“  children ์„ ์ถ”์ถœํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. 
            let scaleResults = response
                .results?.compactMap({ $0 as? ORKScaleQuestionResult }),

            // ๐Ÿ”ฅ pain answer ์€ ํ•ด๋‹น ์‹๋ณ„์ž๋ฅผ ๊ฐ€์ง„ ์ฒซ๋ฒˆ์งธ ๊ฒƒ์ด๊ณ , sleep answer ์€ ํ•ด๋‹น ์‹๋ณ„์ž๋ฅผ ๊ฐ€์ง„ ์ฒซ๋ฒˆ์งธ ๊ฒƒ ์ž…๋‹ˆ๋‹ค.
            let painAnswer = scaleResults
                .first(where: { $0.identifier == checkInPainItemIdentifier })?
                .scaleAnswer,

            let sleepAnswer = scaleResults
                .first(where: { $0.identifier == checkInSleepItemIdentifier })?
                .scaleAnswer
        else {
            assertionFailure("Failed to extract answers from check in survey!")
            return nil
        }

// ๐Ÿ”ฅ CareKit ๊ฒฐ๊ณผ๊ฐ’์œผ๋กœ ๋ณ€ํ™˜ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
// kind ํ”„๋กœํผํ‹ฐ๋Š” ์„ ํƒ์‚ฌํ•ญ์ด์ง€๋งŒ ๋‚˜์ค‘์— ๊ฐ’์„ ์ฐพ์œผ๋ ค๋ฉด ๋„์›€์ด ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.(์ด ๋ถ€๋ถ„์€ part 3 ์—์„œ ์•Œ์•„๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค!)
        var painValue = OCKOutcomeValue(Double(truncating: painAnswer))
        painValue.kind = checkInPainItemIdentifier

        var sleepValue = OCKOutcomeValue(Double(truncating: sleepAnswer))
        sleepValue.kind = checkInSleepItemIdentifier

        return [painValue, sleepValue]
    }

Letโ€™s run the app and see how weโ€™re doing.

12

part 1์—์„œ ์ด๋ฏธ onboarding ์„ ์™„๋ฃŒํ–ˆ๊ธฐ ๋•Œ๋ฌธ์— consent flow ๋ฅผ ๋‹ค์‹œ ํ•˜์ง€ ์•Š์•„๋„ ๋ฉ๋‹ˆ๋‹ค. card ๋ฅผ ํƒญํ•˜๋ฉด ResearchKit survey ๋กœ ์ด๋™ํ•ฉ๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด ํ†ต์ฆ 4, ์ˆ˜๋ฉด์‹œ๊ฐ„ 8 ์„ค์ •ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

13

Care feed ๋กœ ๋Œ์•„๊ฐ€๋ฉด ์œ„์— completion ring ์ด ์ฑ„์›Œ์ง€๋Š” ๊ฒƒ์„ ๋ณผ ์ˆ˜ ์žˆ๊ณ , ResearchKit ์˜ ๋‹ต์ด CareKit ์— ์„ฑ๊ณต์ ์œผ๋กœ ํŒŒ์‹ฑ๋˜์—ˆ์Œ์„ ์•Œ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

14

๋‹ค์ˆ˜์˜ ์งˆ๋ฌธ ํผ์„ ๊ฐ€์ง„ check-in survey ๋ฅผ ๋งˆ์ณค๊ณ , CareKit ์— ์šฐ๋ฆฌ๊ฐ€ ์›ํ•˜๋Š” ๋Œ€๋กœ ์œ ์ง€๋˜๋Š” ๊ฒƒ์„ ํ™•์ธํ–ˆ์Šต๋‹ˆ๋‹ค. ๋ฐ˜์ฏค ์™”๊ตฐ์š”!

15

Create Dynamic Schedules

๋‹ค์Œ์€ CareKit ์˜ ๊ณ ๊ธ‰ ์ผ์ •์œผ๋กœ ๋„˜์–ด๊ฐ€๋ด…์‹œ๋‹ค. ๋‚˜์ค‘์— ์šฐ๋ฆฌ๋Š” range of motion task ๋ฅผ ์ ์šฉํ•  ๊ฒƒ์ž…๋‹ˆ๋‹ค. check-in task ์ด๋‚˜ ์ด์ „์˜ onboarding task ์ฒ˜๋Ÿผ ์ฒซ ๋ฒˆ์งธ๋Š” schedule ์„ ์ •์˜ํ•˜๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ, ์ด ์ž‘์—…์—๋Š” ์ข€ ๋” ๋งŽ์€ ์ž‘์—…์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.

Jamie ๋Š” ์‹œ๊ฐ„์ด ์ง€๋‚จ์— ๋”ฐ๋ผ ์ฐธ๊ฐ€์ž๋“ค์—๊ฒŒ range of motion(์šด๋™ ๋ฒ”์œ„)๋ฅผ ์ธก์ •ํ•˜๋„๋ก ์š”์ฒญํ•˜๋Š” ๋นˆ๋„๋ฅผ ์ค„์ด๋„๋ก ์š”์ฒญํ–ˆ์Šต๋‹ˆ๋‹ค. ๊ตฌ์ฒด์ ์œผ๋กœ, ์šฐ๋ฆฌ๋Š” ์ฐธ๊ฐ€์ž๊ฐ€ ์ฒซ ์ฃผ ๋™์•ˆ ๋งค์ผ range of motion ์„ ์ธก์ •ํ•˜๋„๋ก ํ•˜๋Š” schedule ์„ ์„ธ์› ์œผ๋ฉด ํ•ฉ๋‹ˆ๋‹ค. ๊ทธ ๋‹ค์Œ ์›”๋ง๊นŒ์ง€ ์ผ์ฃผ์ผ์— ํ•œ๋ฒˆ ๊ทธ๋ฆฌ๊ณ  ๊ทธ ์ดํ›„์—๋Š” ๋‹ค์‹œ ์ธก์ •ํ•˜์ง€ ์•Š๋„๋ก ํ–ˆ์œผ๋ฉด ํ•ฉ๋‹ˆ๋‹ค.

์ฒซ ์ฃผ์— ํ•˜๋ฃจ ํ•œ ๋ฒˆ, ๋‚˜๋จธ์ง€ ์ฃผ์—๋Š” ์ฃผ๋งˆ๋‹ค ํ•œ๋ฒˆ. ๋ช‡๊ฐ€์ง€ ์ฃผ์š” ๋‚ ์งœ๋ฅผ ์ •์˜ํ•˜๋Š” ๊ฒƒ์œผ๋กœ ์‹œ์ž‘ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.(thisMorning, nextWeek, and nextMonth)

// 2.6 Add a range of motion task

        let thisMorning = Calendar.current.startOfDay(for: Date())

        let nextWeek = Calendar.current.date(
            byAdding: .weekOfYear,
            value: 1,
            to: Date()
        )!

        let nextMonth = Calendar.current.date(
            byAdding: .month,
            value: 1,
            to: thisMorning
        )

        // ๐Ÿ”ฅ CareKit ์—์„œ ๋ฏธ๋ฌ˜ํ•œ ์ผ์ •์„ ๋งŒ๋“ค๋ ค๋ฉด OCKScheduleElement ๋ฅผ ์‚ฌ์šฉํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
        // schedule element ์—๋Š” ์‹œ์ž‘ ๋‚ ์งœ, ์ข…๋ฃŒ ๋‚ ์งœ๊ฐ€ ์žˆ๊ณ , ํ•ด๋‹น ๊ธฐ๊ฐ„ ๋™์•ˆ iinterval ์„ ๊ฐ€์ง€๊ณ  ๋ฐ˜๋ณต๋ฉ๋‹ˆ๋‹ค.
        let dailyElement = OCKScheduleElement(
            start: thisMorning,
            end: nextWeek,
            interval: DateComponents(day: 1),
            text: nil,
            targetValues: [],
            duration: .allDay
        )

        // ๐Ÿ”ฅ ๋‹ค์Œ์ฃผ๋ถ€ํ„ฐ ์‹œ์ž‘ํ•ด์„œ ๋‹ค์Œ๋‹ฌ๊นŒ์ง€ ๋งค์ฃผ ๋ฐ˜๋ณต๋ฉ๋‹ˆ๋‹ค.
        let weeklyElement = OCKScheduleElement(
            start: nextWeek,
            end: nextMonth,
            interval: DateComponents(weekOfYear: 1),
            text: nil,
            targetValues: [],
            duration: .allDay
        )

        // ๐Ÿ”ฅ ๋‘๊ฐœ์˜ element ๋ฅผ ๊ฐ€์ง€๊ณ  compound schedule ์„ ๋งŒ๋“ค ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
        let rangeOfMotionCheckSchedule = OCKSchedule(
            composing: [dailyElement, weeklyElement]
        )

        // ๐Ÿ”ฅ ๊ทธ๋ฆฌ๊ณ  ํ•ด๋‹น schdule ์„ ์‚ฌ์šฉํ•˜๋Š” range of motion task ๋ฅผ ๋งŒ๋“ค ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
        let rangeOfMotionCheckTask = OCKTask(
            id: TaskIDs.rangeOfMotionCheck,
            title: "Range Of Motion",
            carePlanUUID: nil,
            schedule: rangeOfMotionCheckSchedule
        )

        // ๐Ÿ”ฅ ๋ฌผ๋ก  store ์— ์ถ”๊ฐ€ํ•ด ์ฃผ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
        storeManager.store.addAnyTasks(
            [onboardTask, checkInTask, rangeOfMotionCheckTask],
            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)")
            }
        }

๋‹ค๋ฅธ task ์™€ ๋งˆ์ฐฌ๊ฐ€์ง€๋กœ ๋‹ค์Œ ๋‹จ๊ณ„๋Š” Care feed ๋กœ ์ด๋™ํ•ด์„œ CareKit ์— ์ด ์ž‘์—…์„ ํ‘œ์‹œํ•  ๋ฐฉ๋ฒ•์„ ์ง€์ •ํ•˜๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.

Range of motion

๋‹ค์‹œ SurveyTaskViewController ๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. ๋‹ต๋ณ€์„ ์–ป๊ธฐ์œ„ํ•ด์„œ survey ์™€ function ์„ ์ œ๊ณตํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. Survey.swift ๋กœ ๋Œ์•„๊ฐ€์„œ ์ž‘์—…ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. range of motion tak ๋Š” ์‚ฌ์‹ค์ƒ ๊ฐ„๋‹จํ•ฉ๋‹ˆ๋‹ค.

ResearchKit ์— ๋ฏธ๋ฆฌ ์ •์˜๋˜์–ด์žˆ์Šต๋‹ˆ๋‹ค. ์‹๋ณ„์ž๋ฅผ ์ง€์ •ํ•˜๊ณ , ์ธก์ •ํ•  ๋ฌด๋ฆŽ์„ ์ง€์ •ํ•˜๊ธฐ๋งŒ ํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค.

// MARK: 2.7 Range of Motion

static func rangeOfMotionCheck() -> ORKTask {

        let rangeOfMotionOrderedTask = ORKOrderedTask.kneeRangeOfMotionTask(
            withIdentifier: "rangeOfMotionTask",
            limbOption: .left,
            intendedUseDescription: nil,
            // ๐Ÿ”ฅ ํ•ด๋‹น task ๊ฐ€ ์ฃผ๋Š” ๋ฉ”์‹œ์ง€๋Š” ๊ธฐ๋ณธ๊ฐ’์ด์ง€๋งŒ ์‚ฌ์šฉ์ž ์ง€์ • ๋ฉ”์‹œ์ง€๋ฅผ ๋ณด์—ฌ๋“œ๋ฆฌ๊ธฐ ์œ„ํ•ด์„œ ๋‹ค์Œ ์˜ต์…˜ ์„ค์ •ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค. 
            options: [.excludeConclusion]
        )
        // ๐Ÿ”ฅ ์œ„์˜ ์˜ต์…˜์œผ๋กœ ์„ค์ • ํ›„, ๋ฌผ๋ฆฌ์น˜๋ฃŒ์™€ ๊ด€๋ จ๋œ ํŠน๋ณ„ํ•œ ๊ฒฉ๋ ค์˜ ๋ฉ”์‹œ์ง€๋ฅผ ์‚ฌ์šฉ์ž ์ง€์ •ํ•˜์—ฌ ์ „๋‹ฌํ•  ๊ฒƒ์ž…๋‹ˆ๋‹ค.
        let completionStep = ORKCompletionStep(identifier: "rom.completion")
        completionStep.title = "All done!"
        completionStep.detailText = "We know the road to recovery can be tough. Keep up the good work!"

        rangeOfMotionOrderedTask.appendSteps([completionStep])

        return rangeOfMotionOrderedTask
    }

    // ๐Ÿ”ฅ ResearchKit ๊ฒฐ๊ณผ๊ฐ’์œผ๋กœ ๋ณ€ํ™˜ํ•˜๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.
    static func extractRangeOfMotionOutcome(
        _ result: ORKTaskResult) -> [OCKOutcomeValue]? {

        guard let motionResult = result.results?
            .compactMap({ $0 as? ORKStepResult })
            .compactMap({ $0.results })
            .flatMap({ $0 })
            .compactMap({ $0 as? ORKRangeOfMotionResult })
            // ๐Ÿ”ฅ ์šฐ๋ฆฌ๊ฐ€ ํ•  ์ˆ˜ ์žˆ๋Š” ๋ฐฉ๋ฒ•์€ ๋งŽ์ง€๋งŒ, ์˜ค๋Š˜์€ ์ฒซ ๋ฒˆ์งธ ๋ฒ”์œ„์˜ ์›€์ง์ž„ ๊ฒฐ๊ณผ๊ฐ€ ๋‚˜์˜ฌ ๋•Œ๋งŒ ์‚ฌ์šฉํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค. ๊ทธ๋ฆฌ๊ณ  ์˜ค์ง ํ•˜๋‚˜ ๋ฐ–์— ์—†๋‹ค๋Š” ๊ฒƒ์„ ์•Œ๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.
            .first else {

            assertionFailure("Failed to parse range of motion result")
            return nil
        }

        // ๐Ÿ”ฅ range of motion ์˜ ๊ฒฐ๊ณผ๋Š” ์œ ์šฉํ•œ ํŠน์„ฑ๋“ค์„ ๊ฐ€์ง€๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ, ์šฐ๋ฆฌ์˜ use case ์—์„œ ๊ฐ€์žฅ ๊ด€์‹ฌ ์žˆ๋Š” ๊ฒƒ์€ range ์ž…๋‹ˆ๋‹ค. ์ด๊ฒƒ์€ ์ฐธ๊ฐ€์ž๊ฐ€ ๋ฌด๋ฆŽ์„ ์–ผ๋งˆ๋‚˜ ๊ตฌ๋ถ€๋ฆด ์ˆ˜ ์žˆ์—ˆ๋Š”์ง€ ์ธก์ •ํ•˜๋Š” ๋ฒ”์œ„์ž…๋‹ˆ๋‹ค.
        var range = OCKOutcomeValue(motionResult.range)
// ๐Ÿ”ฅ ์ด ๊ฐ’์€ ๋‚˜์ค‘์— ์‰ฝ๊ฒŒ ์ฐพ์„ ์ˆ˜ ์žˆ๋„๋ก keyPath ์™€ ํ•จ๊ป˜ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. part 3 ์—์„œ ์ข€ ๋” ์•Œ์•„๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.
        range.kind = #keyPath(ORKRangeOfMotionResult.range)

        return [range]
    }

๋ชจ๋“  ์ค€๋น„๋ฅผ ๋งˆ์นœ ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค.

๋จผ์ € ์Šค์ผ€์ค„์ด ์šฐ๋ฆฌ๊ฐ€ ์˜๋„ํ•˜๋Š” ๋Œ€๋กœ ์ž‘๋™ํ•˜๊ณ  ์žˆ๋Š”์ง€ ํ™•์ธํ•ด๋ณผ ํ•„์š”๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค. range of motion task ๋Š” ๋งค์ผ ํ‘œ์‹œ๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋‚˜ ๋‹ค์Œ์ฃผ๋กœ ๋„˜์–ด๊ฐ€๋ฉด ์›”์š”์ผ์„ ์ œ์™ธํ•˜๊ณ  ํ‘œ์‹œ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๋” ๋‚˜์•„๊ฐ€์„œ ๋‹ค์Œ ๋‹ฌ์—๋Š” ๋” ์ด์ƒ ๋‚˜ํƒ€๋‚˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

CareKit shcedules ๋Š” ์ด๋Ÿฌํ•œ ์š”๋ฒ•์„ ๋ฏธ๋ฆฌ ํ”„๋กœ๊ทธ๋ž˜๋ฐํ•  ์ˆ˜ ์žˆ๋Š” ์ข‹์€ ๋ฐฉ๋ฒ•์ž…๋‹ˆ๋‹ค.

6์›” 7์ผ ์›”์š”์ผ ๊ณผ 6์›” 9์ผ์—๋Š” ์ฒซ์ฃผ์ด๊ธฐ ๋•Œ๋ฌธ์— ๋งค์ผ๋งค์ผ check-in ๋‚˜ํƒ€๋‚œ๋‹ค.

แ„‰แ…ณแ„แ…ณแ„…แ…ตแ†ซแ„‰แ…ฃแ†บ 2022-06-07 แ„‹แ…ฉแ„’แ…ฎ 10 04 22

6์›” 14์ผ ์›”์š”์ผ์ด๋ผ ํ‘œ์‹œ๋˜๊ณ , 6์›” 17์ผ์€ ํ‘œ์‹œ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

แ„‰แ…ณแ„แ…ณแ„…แ…ตแ†ซแ„‰แ…ฃแ†บ 2022-06-07 แ„‹แ…ฉแ„’แ…ฎ 10 04 31

๋‹ค์Œ ๋‹ฌ์—๋Š” ํ‘œ์‹œ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

1 11 22 33 44