WWDC Quick Look 💓 By SwiftGGTeam
Build a research and care app, part 3: Visualize progress

Build a research and care app, part 3: Visualize progress

元の動画を見る

ハイライト

Apple は、ResearchKit を CareKit とどのように組み合わせることができるかをデモしました。開発者はタスク カードをカスタマイズし、調査編集を制限し、調査回答をグラフ化し、ResearchKit を使用して 3D 教育コンテンツを提示できます。

主要内容

前回のエピソード以来、回復研究アプリである Recover を使用して、参加者が毎日のアンケートと可動域テストに記入できるようになりました。実際に使用すると問題が発生します。

参加者が前日の記録を振り返ると、「完了」のみが表示されます。彼らは、その日にどれだけの痛みを解消したか、何時間眠ったかを知ることができません。過去の回答を削除したり、将来の日付のアンケートを事前に入力したりすることもできます。これは研究データにとって好ましくありません。

医師も傾向を知りたいと考えています。単一の回答では 1 日の状態しか説明できませんが、回復が進んでいるかどうかを示すことはできません。ジェイミーは、[インサイト] タブに 2 つのグラフを表示したいと考えています。1 つは睡眠と痛みを比較し、もう 1 つは時間の経過に伴う可動域を示します。

Apple のアプローチは、ResearchKit (調査調査フレームワーク) と CareKit (ケア プランニング フレームワーク) を接続することです。 ResearchKit は、アンケート、アクティブなタスク、および 3D 教育コンテンツを担当します。 CareKit は、スケジュール、タスク カード、データ ストレージ、グラフを処理します。

このセッションの焦点は、収集したデータを読み取り可能なフィードバックに変換することです。参加者がアンケートを完了すると、カードに回答が直接表示されます。 Insights に入ると、チャートは CareKit ストアから履歴結果を自動的に読み取ります。教育カードをクリックすると、ResearchKit に説明テキストと 3D 膝モデルが表示されます。

タスク カードが「完了ステータス」から「結果の概要」に変わります。

以前のカードは、タスクが完了したかどうかを参加者に伝えるだけでした。完了情報は研究者にとっては役立ちますが、参加者にとっては限定的な助けとなります。参加者が本当に知りたいのは、「その日に私が何を記入したか」です。

CareKit を使用すると、開発者は View Synchronizer を通じてタスク カードをカスタマイズできます。 Store データが変更されると UI が更新されます。ここに継承されましたOCKSurveyTaskViewSynchronizer、存在するupdateViewからアンケート結果を読み取ってから書き込みますinstructionsLabel

アンケートは適切な日付にのみ編集できます

医学研究データには時間境界が必要です。過去のリハビリテーション検査をやり直すことはできません。また、将来の質問票に事前に記入することもできません。

コードには 2 つのレベルの制限があります。履歴結果を削除する場合は、OCKSurveyTaskViewControllerDelegate戻るfalse。将来の日付のタスクを表示する場合は、カードのインタラクションを無効にして透明度を下げます。

「インサイト」タブにはトレンドが表示されます

単一の痛みのスコアは単なるポイントです。連続データをグラフに入力すると、患者と医師は痛み、睡眠、可動域の変化を確認できるようになります。

ケアキットOCKCartesianChartViewControllerストア内のタスクの結果を直接利用できます。開発者は、データ系列を構成し、どのタスクからデータを取得するか、毎日の Y 値を計算する方法をチャートに指示するだけで済みます。

ResearchKit は教育コンテンツも表示できます

医師は患者に半月板損傷を理解してもらいたいと考えています。このコンテンツにはスケジュールがなく、結果を保存する必要もなく、毎日のタスクでもありません。

したがって、CareKit スケジュールには入りません。アプリはインサイトに 1 つを追加しますOCKFeaturedContentView、ユーザーがクリックした後に作成されますORKTaskViewControllerでは、説明ステップと 3D モデル ステップを含む ResearchKit タスクを実行します。

詳細

アンケート カードをカスタマイズして送信された回答を表示する

(02:27) CareKit のタスク カードは、データ シンクロナイザーとビュー シンクロナイザーの両方によって駆動されます。OCKSurveyTaskViewSynchronizerデフォルトのスタイルを提供し、サブクラスはupdateView独自の UI ロジックを追加します。

final class SurveyViewSynchronizer: OCKSurveyTaskViewSynchronizer {

    override func updateView(
        _ view: OCKInstructionsTaskView,
        context: OCKSynchronizationContext<OCKTaskEvents>) {

        super.updateView(view, context: context)

        if let event = context.viewModel.first?.first, event.outcome != nil {
            view.instructionsLabel.isHidden = false

            let pain = event.answer(kind: Surveys.checkInPainItemIdentifier)
            let sleep = event.answer(kind: Surveys.checkInSleepItemIdentifier)

            view.instructionsLabel.text = """
                Pain: \(Int(pain))
                Sleep: \(Int(sleep)) hours
                """
        } else {
            view.instructionsLabel.isHidden = true
        }
    }
}

キーポイント:

  • SurveyViewSynchronizer継承するOCKSurveyTaskViewSynchronizer、CareKit のデフォルトの調査カードの動作を再利用します。
  • updateViewStore データが変更されたときに呼び出され、UI に結果を表示するのに適しています。
  • super.updateView(view, context: context)システムのデフォルトの更新ロジックを最初に維持します。
  • context.viewModel.first?.first現在のカードに対応するイベントを取得します。
  • event.outcome != nilこのイベントが完了したことを示します。
  • event.answer(kind:)結果の価値によるkind痛みと睡眠の答えを読んでください。
  • instructionsLabel.text参加者が過去の日付を振り返るときに具体的な値を確認できるように、答えをカードに書き戻します。
  • 結果がない場合は非表示にしますinstructionsLabel、空白の概要が表示されるのを避けるため。

(03:12) ここで使用できますkind前のパートでアンケート結果がそれぞれ保存されるので、回答を確認してください。OCKOutcomeValueセットkind

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

    guard
        let response = result.results?
            .compactMap({ $0 as? ORKStepResult })
            .first(where: { $0.identifier == checkInFormIdentifier }),

        let scaleResults = response
            .results?.compactMap({ $0 as? ORKScaleQuestionResult }),

        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
    }

    var painValue = OCKOutcomeValue(Double(truncating: painAnswer))
    painValue.kind = checkInPainItemIdentifier

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

    return [painValue, sleepValue]
}

キーポイント:

  • ORKTaskResultResearchKit 調査が完了した後の結果オブジェクトです。
  • ORKStepResultチェックイン フォームのステップを見つけます。
  • ORKScaleQuestionResultスケールに関する質問への回答を読んでください。
  • painAnswerそしてsleepAnswerそれぞれ痛みに関する質問と睡眠に関する質問から。
  • OCKOutcomeValue(Double(truncating:))ResearchKit 値を CareKit 結果値に変換します。
  • kindフィールド識別子は保存され、以降のカードやチャートで痛みと睡眠を区別するために使用されます。
  • 返される配列は次のようになります。OCKSurveyTaskViewControllerCareKit ストアにメールしてください。

(03:37) カスタム シンクロナイザーを有効にするには、アンケート カードの作成時にカスタム シンクロナイザーを渡す必要があります。viewSynchronizer、設定しますsurveyDelegate

let survey = OCKSurveyTaskViewController(
    task: task,
    eventQuery: OCKEventQuery(for: date),
    storeManager: storeManager,
    survey: Surveys.checkInSurvey(),
    viewSynchronizer: SurveyViewSynchronizer(),
    extractOutcome: Surveys.extractAnswersFromCheckInSurvey
)
survey.surveyDelegate = self

キーポイント:

  • taskCareKit ストアでのチェックイン タスクです。
  • OCKEventQuery(for: date)現在の日付に限定されたイベント。
  • storeManagerタスク カードとストアを同期させます。
  • surveyResearchKit のアンケートが利用可能です。
  • viewSynchronizerデフォルトのビュー同期ロジックを置き換えます。
  • extractOutcomeResearchKit の結果が CareKit の結果に変換される方法を定義します。
  • surveyDelegateその後の削除権限と完了コールバックの制御に使用されます。

過去の結果を削除したり、将来のタスクを事前に記入したりすることは禁止されています。

(03:42) CareKit は結果を削除する前にデリゲートに質問します。このメソッドは、イベントの結果の削除が許可されるかどうかを決定するブール値を返します。

func surveyTask(
    viewController: OCKSurveyTaskViewController,
    shouldAllowDeletingOutcomeForEvent event: OCKAnyEvent) -> Bool {

    event.scheduleEvent.start >= Calendar.current.startOfDay(for: Date())
}

キーポイント:

  • からのメソッドOCKSurveyTaskViewControllerDelegate
  • event.scheduleEvent.startタスクイベントの開始時刻です。
  • Calendar.current.startOfDay(for: Date())今日は0点でした。
  • イベントの開始時刻が今日より前の場合に式が返されますfalse, CareKitでは削除ができません。
  • 今日以降のイベントの再開trueの場合でも、現在の日付をやり直すか削除することができます。

(04:28) 将来の日付を持つタスクは、リストの生成時に処理されます。このコードは、まずリストの日付が今日より後であるかどうかを判断し、次にカード インタラクション ステータスを一律に変更します。

let isFuture = Calendar.current.compare(
    date,
    to: Date(),
    toGranularity: .day) == .orderedDescending

self.fetchTasks(on: date) { tasks in
    tasks.compactMap {

        let card = self.taskViewController(for: $0, on: date)
        card?.view.isUserInteractionEnabled = !isFuture
        card?.view.alpha = isFuture ? 0.4 : 1.0

        return card

    }.forEach {
        listViewController.appendViewController($0, animated: false)
    }
}

キーポイント:

  • Calendar.current.compare日ごとに比較するdate時と分を無視した現在時刻。
  • isFutureのためにtrueユーザーが将来の日付を表示していることを示します。
  • fetchTasks(on:)CareKit ストアからその日のイベントを含むタスクを取得します。
  • taskViewController(for:on:)各タスクに対応するカードを生成します。
  • isUserInteractionEnabled = !isFuture今後カードがクリックされたり、入力されたりするのを防ぎます。
  • alpha = 0.4ユーザーに視覚的なフィードバックを提供します。タスクはプレビューできますが、操作はできません。
  • appendViewController処理済みのカードを毎日のリストに追加します。

CareKit を使用して痛みと睡眠をグラフ化する

(05:42) Insights の最初の画像は 2 セットの棒グラフです。同じチェックイン タスクから、痛みのスコアと睡眠時間という 2 つのカテゴリの回答が必要になります。

let painSeries = OCKDataSeriesConfiguration(
    taskID: TaskIDs.checkIn,
    legendTitle: "Pain (1-10)",
    gradientStartColor: #colorLiteral(red: 1, green: 0.462745098, blue: 0.368627451, alpha: 1),
    gradientEndColor: #colorLiteral(red: 1, green: 0.462745098, blue: 0.368627451, alpha: 1),
    markerSize: 10,
    eventAggregator: .custom({ events in
        events
            .first?
            .answer(kind: Surveys.checkInPainItemIdentifier)
        ?? 0
    })
)

let sleepSeries = OCKDataSeriesConfiguration(
    taskID: TaskIDs.checkIn,
    legendTitle: "Sleep (hours)",
    gradientStartColor: UIColor.systemBlue,
    gradientEndColor: UIColor.systemBlue,
    markerSize: 10,
    eventAggregator: .custom({ events in
        events
            .first?
            .answer(kind: Surveys.checkInSleepItemIdentifier)
        ?? 0
    })
)

let barChart = OCKCartesianChartViewController(
    plotType: .bar,
    selectedDate: Date(),
    configurations: [painSeries, sleepSeries],
    storeManager: storeManager
)

appendViewController(barChart, animated: false)

キーポイント:

  • OCKDataSeriesConfigurationグラフ内の一連のデータを説明します。
  • taskID: TaskIDs.checkInデータ ソースの指定は毎日のチェックイン タスクです。
  • legendTitle凡例のテキストを表示します。
  • gradientStartColorそしてgradientEndColor列の色を制御します。
  • markerSize: 10列の幅を制御します。
  • eventAggregator: .custom1 日内の複数のイベントが 1 つの Y 値にどのように変換されるかを定義します。
  • ここでのチェックイン イベントは 1 日に 1 回のみですので、必ずお読みください。events.first
  • 痛みシリーズの読書checkInPainItemIdentifier、睡眠シリーズ読書checkInSleepItemIdentifier
  • OCKCartesianChartViewController(plotType: .bar)ヒストグラムを作成します。
  • storeManagerストアのデータが変更されたときにグラフを自動的に更新します。

散布図を使用して可動域の回復を表示する

(07:14) 2 番目の写真は可動域テストの結果を示しています。必要なのはデータ系列のみで、グラフの種類は散布図に変更されます。

let rangeSeries = OCKDataSeriesConfiguration(
    taskID: TaskIDs.rangeOfMotionCheck,
    legendTitle: "Range of Motion (degrees)",
    gradientStartColor: view.tintColor,
    gradientEndColor: view.tintColor,
    markerSize: 3,
    eventAggregator: .custom({ events in
        events
            .first?
            .answer(kind: #keyPath(ORKRangeOfMotionResult.range))
        ?? 0
    })
)

let scatterChart = OCKCartesianChartViewController(
    plotType: .scatter,
    selectedDate: Date(),
    configurations: [rangeSeries],
    storeManager: storeManager
)

appendViewController(scatterChart, animated: false)

キーポイント:

  • taskID: TaskIDs.rangeOfMotionCheckResearchKit の膝関節可動域タスクを指します。
  • legendTitle単位は度(角度)です。
  • markerSize: 3散乱を小さくします。
  • eventAggregatorその日の最初の可動域イベントを読んでください。
  • #keyPath(ORKRangeOfMotionResult.range)結果を保存するときに使用されますkind位置合わせ。
  • plotType: .scatterデータを散布点として表示させます。
  • configurations: [rangeSeries]このピクチャには 1 セットのデータしかないことを示します。

(08:04) 図は CareKit ストアに接続されています。デモで火曜日の 2 つの結果を削除した後、インサイトに戻ると、火曜日のデータ ポイントが自動的に消えます。

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
    }

    var range = OCKOutcomeValue(motionResult.range)
    range.kind = #keyPath(ORKRangeOfMotionResult.range)

    return [range]
}

キーポイント:

  • ORKRangeOfMotionResultResearchKit アクティブ タスクによって生成されたアクティビティ スコープの結果です。
  • motionResult.rangeはテストから得られた角度の値です。
  • OCKOutcomeValue(motionResult.range)結果を CareKit が保存できる値として書き込みます。
  • range.kind使用#keyPath(ORKRangeOfMotionResult.range)、チャートは同じキーに従って読み取られます。
  • アレイを返却した後は、CareKit Store が永続化を担当します。

注目のコンテンツを含むオープン 3D 教育コンテンツ

(08:23) 教育コンテンツには固定時間がなく、保存する結果もありません。 Insights に配置するのに適しています。OCKFeaturedContentView入り口として。

let kneeModelView = OCKFeaturedContentView(imageOverlayStyle: .dark)
kneeModelView.delegate = self
kneeModelView.label.text = "About the meniscus"
kneeModelView.label.textColor = .systemBackground
kneeModelView.imageView.image = UIImage(named: "knee")
appendView(kneeModelView, animated: true)

キーポイント:

  • OCKFeaturedContentViewCareKit のコンテンツ カード ビューです。
  • imageOverlayStyle: .dark画像にダークマスクを追加して、テキストを表示しやすくします。
  • delegate = selfコントローラーがクリック イベントを受信できるようにします。
  • label.textカードタイトルを設定します。
  • imageView.image表紙画像を設定します。
  • appendViewカードをインサイト リストに追加します。

(09:12) ユーザーがカードをクリックすると、アプリは ResearchKit タスクを作成し、ORKTaskViewController展示する。

func didTapView(_ view: OCKFeaturedContentView) {

    let humanModelTask = Surveys.kneeModel()

    let taskViewController = ORKTaskViewController(
        task: humanModelTask,
        taskRun: nil
    )

    taskViewController.delegate = self

    present(taskViewController, animated: true, completion: nil)
}

func taskViewController(
    _ taskViewController: ORKTaskViewController,
    didFinishWith reason: ORKTaskViewControllerFinishReason,
    error: Error?) {

    taskViewController.dismiss(animated: true, completion: nil)
}

キーポイント:

  • didTapViewからOCKFeaturedContentViewDelegate
  • Surveys.kneeModel()ResearchKit タスクを返します。
  • ORKTaskViewControllerこのタスクの実行を担当します。
  • taskRun: nil独立した実行 ID を指定する必要がないことを示します。
  • delegate = self完了コールバックを受信するために使用されます。
  • present教育コンテンツをモーダルで開きます。
  • didFinishWith閉じる場合、ユーザーはモデルを表示した後、Insights に戻ります。

ResearchKit 3D モデルの作成タスク

(09:53) 3D 教育コンテンツは、コンテキストを説明する説明ステップと、3D モデルのステップの 2 つのステップで構成されます。

static func kneeModel() -> ORKTask {

    let instructionStep = ORKInstructionStep(
        identifier: "insights.instructionStep"
    )
    instructionStep.title = "Your Injury Visualized"
    instructionStep.detailText = "A 3D model will be presented to give you better insights on your specific injury."
    instructionStep.iconImage = UIImage(systemName: "bandage")

    let modelManager = ORKUSDZModelManager(usdzFileName: "toy_robot_vintage")

    let kneeModelStep = ORK3DModelStep(
        identifier: "insights.kneeModel",
        modelManager: modelManager
    )

    let kneeModelTask = ORKOrderedTask(
        identifier: "insights",
        steps: [instructionStep, kneeModelStep]
    )

    return kneeModelTask
}

キーポイント:

  • ORKInstructionStepタイトル、説明、アイコンを表示します。
  • identifierステップに安定した ID を与えます。
  • ORKUSDZModelManagerローカルの USDZ モデルをロードします。
  • 最初にサンプルコードを使用してくださいtoy_robot_vintage3D モデル プロセスが利用可能であることを確認します。
  • ORK3DModelStepResearchKitの3Dモデル表示ステップです。
  • ORKOrderedTask指示ステップとモデルステップを順番に整理します。
  • セッションの後半で、最後のデモンストレーションでは、おもちゃのロボットが BioDigital のモデル マネージャーに置き換えられ、膝と半月板のモデルを表示するために使用されたと述べられました。

重要ポイント

  1. やるべきこと: リハビリアプリの毎日のアンケートカードに「その日の回答のまとめ」を追加します。 実施する価値がある理由: 参加者が過去の日付を振り返ると、痛みや睡眠などの具体的な回答がわかるため、「完了したことだけはわかっているが、何を記入すればよいのかわからない」という問題が軽減されます。 開始方法: 継承OCKSurveyTaskViewSynchronizer、存在するupdateView読み込むevent.answer(kind:)、結果をに書き込みますinstructionsLabel

  2. 対処方法: ユーザーによる過去の健全性テスト結果の変更を無効にし、今後のタスクを無効にします。 実行する価値がある理由: リハビリテーション、臨床研究、症状追跡はすべて、日付ごとに記録されたデータに依存しています。過去および将来のデータをランダムに変更すると、傾向の判断に影響します。 開始方法: 実装surveyTask(_:shouldAllowDeletingOutcomeForEvent:)、使用Calendar.current.startOfDay(for:)歴史上の出来事を特定する。将来の日付カードを生成するときに設定しますisUserInteractionEnabled = false

  3. やるべきこと: Insights ページで症状と生活習慣の比較表を作成します。 実行する価値がある理由: 痛みと睡眠を同じヒストグラムに入れると、睡眠不足後に痛みが増加するかどうかを参加者が確認しやすくなります。 開始方法: 同じタスクに対して 2 つ作成しますOCKDataSeriesConfiguration、異なるeventAggregatorさまざまな結果を読むkind、そしてそれを渡しますOCKCartesianChartViewController(plotType: .bar)

  4. やるべきこと: アクティブなタスクの結果を回復の進行状況の散布図に作成します。 実行する価値がある理由: 可動域 このタイプの検査には自然な時間傾向があり、散布図により医師と患者は回復の軌跡を確認できます。 開始方法:からORKRangeOfMotionResult.range作成するOCKOutcomeValue、設定kind = #keyPath(ORKRangeOfMotionResult.range)、次に使用しますplotType: .scatter描く。

  5. やるべきこと: 3D 解剖学教育ポータルを患者に追加します。 価値がある理由: 医師が提供するテキストによる説明と 3D モデルの組み合わせにより、患者は傷害の場所と回復目標を理解するのに役立ちます。 開始方法: CareKit にインサイトを追加するOCKFeaturedContentView、「プレゼント」をクリックした後ORKTaskViewController、タスクには以下が含まれますORKInstructionStepそしてORK3DModelStep

関連セッション

コメント

GitHub Issues · utterances