LoginSignup
6
5

More than 3 years have passed since last update.

iOS ResearchKit を利用して非公式の医療研究試験を実施する

Last updated at Posted at 2020-05-22

ResearchKit とは?

ResearchKitApple が医学研究目的で開発したオープンソースのフレームワークで、多くの機能が搭載されています。ここでは、基本的な医学的検査を例に、このフレームワークの使い方を紹介します。ここで得られる検査結果は公式のものではなく、健康状態を結論づける公式なデータとしては使えないことにご注意ください。

本フレームワークは英語のみで提供されていますが、一部の検査は言語非依存です。

学習内容

  • (記憶)空間記憶
  • タッピングスピード (iPhone)
  • ハノイの塔
  • トレイルメイキング(数字と文字を順に結んでいく)
  • 視覚コントラスト検査
  • 視力検査
  • PSAT(前の2つの数字を合計)
  • (聴覚)トーン聴覚検査
  • (聴覚)dBHLトーン聴覚検査

警告

これらの検査は研究目的のみで実施されるものです!医学的検査を実施できるのは医師のみです。本記事は、このフレームワークの機能の使用法の紹介のみを目的としています。

実装

ステップ1. ResearchKit フレームワークを複製する

ResearchKit フレームワークを既存のプロジェクトに複製します: git@github.com:ResearchKit/ResearchKit.git

ステップ2. フレームワークを Xcode に追加する

次のように、フレームワーク Xcode のプロジェクトファイルをあなたのプロジェクトファイルにドラッグします:

Screen Shot 2020-05-21 at 18.41.27.png

次に、ResearchKit のビルドされた製品をプロジェクトターゲットのフレームワーク、ライブラリ、埋め込みコンテンツセクションに追加します:
「+」アイコンをクリックし、 ResearchKit を選択します。 そして、タイプを Embed & Sign に変更します

Screen Shot 2020-05-21 at 18.49.15.png

ステップ3. テストコードを追加して結果を受け取る

テストを提示し、その結果を受け取る基本的な構造は次のとおりです:

import ResearchKit

テストビューを表示:

let taskViewController = ORKTaskViewController(task: task, taskRun: nil)
taskViewController.outputDirectory = FileManager.default.temporaryDirectory
taskViewController.delegate = self
present(alert, animated: true, completion: nil)

結果を確認するには:

extension testingTableView: ORKTaskViewControllerDelegate {

    func taskViewController(_ taskViewController: ORKTaskViewController, didFinishWith reason: ORKTaskViewControllerFinishReason, error: Error?) {
        taskViewController.dismiss(animated: true, completion: nil)
    }

    func taskViewController(_ taskViewController: ORKTaskViewController, didChange result: ORKTaskResult) {
        //結果はここで受け取られ、`result.identifier` で取り込むことができます
        //特定のテストの結果を受け取る方法について説明します。
    }

}

各テストの固有コード

(記憶)空間記憶

ezgif-2-fd6c1f9c1cc7.gif

let task = ORKOrderedTask.spatialSpanMemoryTask(withIdentifier: "spatialSpanMemory", intendedUseDescription: nil, initialSpan: 2, minimumSpan: 1, maximumSpan: 5, playSpeed: 1, maximumTests: 8, maximumConsecutiveFailures: 2, customTargetImage: nil, customTargetPluralName: nil, requireReversal: false, options: [])
let taskViewController = ORKTaskViewController(task: task, taskRun: nil)
taskViewController.outputDirectory = FileManager.default.temporaryDirectory
taskViewController.delegate = self
present(alert, animated: true, completion: nil)
extension testingTableView: ORKTaskViewControllerDelegate {
    ...
    func taskViewController(_ taskViewController: ORKTaskViewController, didChange result: ORKTaskResult) {
        if result.identifier == "spatialSpanMemory" {
            if let memoryResult = result.stepResult(forStepIdentifier: "cognitive.memory.spatialspan")?.results?.first as? ORKSpatialSpanMemoryResult {
                let score = String(memoryResult.score) //スコア
                let numFailed = String(memoryResult.numberOfFailures) //失敗したカウント
                let totalNum = String(memoryResult.numberOfGames) //総数
                //TODO
            }
        }
    }
    ...
}

タッピングスピード (iPhone)

picture

let task = ORKOrderedTask.twoFingerTappingIntervalTask(withIdentifier: "tappingTest", intendedUseDescription: nil, duration: 10, handOptions: .both, options: [])
//ORKTaskViewController...
//delegate...
func taskViewController(_ taskViewController: ORKTaskViewController, didChange result: ORKTaskResult) {
    if result.identifier == "tappingTest" {
        if let leftHandResults = result.stepResult(forStepIdentifier: "tapping.left")?.results,
            let rightHandResults = result.stepResult(forStepIdentifier: "tapping.right")?.results {
            if leftHandResults.count == 2 && rightHandResults.count == 2 {
                let leftClickCount = (leftHandResults[1] as? ORKTappingIntervalResult)?.samples?.count ?? 0 //左側の数
                let rightClickCount = (rightHandResults[1] as? ORKTappingIntervalResult)?.samples?.count ?? 0 //右側の数
                //TODO
            }
        }
    }
}
//...

ハノイの塔

picture

let task = ORKOrderedTask.towerOfHanoiTask(withIdentifier: "towerOfHanoi", intendedUseDescription: nil, numberOfDisks: 4, options: [])
//ORKTaskViewController...
//delegate...
func taskViewController(_ taskViewController: ORKTaskViewController, didChange result: ORKTaskResult) {
    if result.identifier == "towerOfHanoi" {
        if let stepResult = result.stepResult(forStepIdentifier: "towerOfHanoi")?.results?.first as? ORKTowerOfHanoiResult,
            let moves = stepResult.moves,
            let time = moves.last?.timestamp {
            let movesTaken = String(moves.count)
            let timeCompleted = String(format: "%.2f", time)
            //TODO
        }
    }
}
//...

トレイルメイキング(数字と文字を順に結んでいく)

picture

let task = ORKOrderedTask.trailmakingTask(withIdentifier: "trailMaking", intendedUseDescription: nil, trailmakingInstruction: nil, trailType: .B, options: [])
//ORKTaskViewController...
//delegate...
func taskViewController(_ taskViewController: ORKTaskViewController, didChange result: ORKTaskResult) {
    if result.identifier == "trailMaking" {
        if let trailResult = result.stepResult(forStepIdentifier: "trailmaking")?.results?.first as? ORKTrailmakingResult,
            let timeTaken = trailResult.taps.last?.timestamp {
            let numErrors = trailResult.numberOfErrors //エラー数
            //TODO
        }
    }
}
//...

視覚コントラスト検査

picture

let task = ORKOrderedTask.landoltCContrastSensitivityTask(withIdentifier: "landoltCcontrast", intendedUseDescription: nil)
//ORKTaskViewController...
//delegate...
func taskViewController(_ taskViewController: ORKTaskViewController, didChange result: ORKTaskResult) {
    if result.identifier == "landoltCcontrast" {
        if let stepResults = result.stepResult(forStepIdentifier: "landoltCStep")?.results {
            var passedTestCount = 0
            for stepResult in stepResults {
                if let convertedResult = stepResult as? ORKLandoltCResult {
                    if (convertedResult.outcome ?? false) { passedTestCount += 1}
                }
            }
            //TODO: `passedTestCount` 正しい選択の数, `String(stepResults.count)` 総数
        }
    }
}
//...

視力検査

picture

let task = ORKOrderedTask.landoltCVisualAcuityTask(withIdentifier: "visualAcuity", intendedUseDescription: nil)
//ORKTaskViewController...
//delegate...
func taskViewController(_ taskViewController: ORKTaskViewController, didChange result: ORKTaskResult) {
    if result.identifier == "visualAcuity" {
        if let visualResults = result.stepResult(forStepIdentifier: "landoltCStep")?.results {
            var passed = 0
            for result in visualResults {
                if let convertedResult = result as? ORKLandoltCResult {
                    if (convertedResult.outcome ?? false) {
                        passed += 1
                    }
                }
            }
            //TODO: `passed` は、合格したテストの数を意味します。
        }
    }
}
//...

PSAT(前の2つの数字を合計)

picture

let task = ORKOrderedTask.psatTask(withIdentifier: "psat", intendedUseDescription: nil, presentationMode: .visual, interStimulusInterval: 4.5, stimulusDuration: 4.5, seriesLength: 12, options: [])
//ORKTaskViewController...
//delegate...
func taskViewController(_ taskViewController: ORKTaskViewController, didChange result: ORKTaskResult) {
    if result.identifier == "psat" {
        if let stepResult = result.stepResult(forStepIdentifier: "psat")?.results?.first as? ORKPSATResult {
            let totalCount = String(stepResult.totalCorrect) //総数
            let correctCount = String(stepResult.samples?.count ?? 0) //正しい選択の数
            //TODO
        }
    }
}
//...

(聴覚)トーン聴覚検査

picture

Present the test view:

let task = ORKOrderedTask.toneAudiometryTask(withIdentifier: "toneAudiometry", intendedUseDescription: nil, speechInstruction: nil, shortSpeechInstruction: nil, toneDuration: 60, options: [])
//ORKTaskViewController...

Checking the result

//delegate...
func taskViewController(_ taskViewController: ORKTaskViewController, didChange result: ORKTaskResult) {
       if let audioResults = result.stepResult(forStepIdentifier: "tone.audiometry")?.results {
           var matchedCount = 0 //正しい選択の数
           var missedChannel = [String]() //失敗したオーディオチャンネル
           guard let audioResult = audioResults.first as? ORKToneAudiometryResult else { return }
           guard let audioSamples = audioResult.samples else { return }
           let totalTestCount = audioSamples.count //実施されたテストの総数
           for sample in audioSamples {
               if sample.channel == sample.channelSelected {
                   matchedCount += 1 //一致した
               } else {
                   missedChannel.append("周波数 " + String(format: "%.2f", sample.frequency)) //違う
               }
           }
       }
   }
//...

(聴覚)dBHLトーン聴覚検査

let task = ORKOrderedTask.dBHLToneAudiometryTask(withIdentifier: "dBHLToneAudiometry", intendedUseDescription: nil, options: [])
//ORKTaskViewController...
//delegate...
func taskViewController(_ taskViewController: ORKTaskViewController, didChange result: ORKTaskResult) {
    if result.identifier == "dBHLToneAudiometry" {
        var resultStr = ""
        if let stepResult1 = result.stepResult(forStepIdentifier: "dBHL1.tone.audiometry") {
            if let firstResult = stepResult1.results?.first as? ORKdBHLToneAudiometryResult {
                resultStr += "オーディオチャンネル 0:\n" + processSingleDBHLAudioResult(firstResult: firstResult) + "\n"
            }
        }
        if let stepResult2 = result.stepResult(forStepIdentifier: "dBHL2.tone.audiometry") {
            if let secondResult = stepResult2.results?.first as? ORKdBHLToneAudiometryResult {
                resultStr += "オーディオチャンネル 1:\n" + processSingleDBHLAudioResult(firstResult: secondResult) + "\n"
            }
        }
    }
}

func processSingleDBHLAudioResult(firstResult: ORKdBHLToneAudiometryResult) -> String {
    var resultStr = ""
    if let samples = firstResult.samples {
        /*
         各サンプルには異なる可聴周波数が含まれています
         */
        for sample in samples {
            var subResultStr = "\n周波数 " + String(sample.frequency) + "\n"
            /*
             単位は、周波数内での異なるdB(音量レベル)のテストを指します
             */
            if let units = sample.units {
                var successfulTests = 0
                for unit in units {
                    let dBValue = String(format: "%.2f", unit.dBHLValue)
                    let userTapped = (unit.userTapTimeStamp != Double.zero)
                    if userTapped { successfulTests += 1 }
                    subResultStr += dBValue + " dB " + userTapped.getYesNo() + ", "
                }
                subResultStr += String(successfulTests) + " / " + String(units.count) + " 合格しました" + "\n"
            }
            resultStr += subResultStr
        }
    }
    return resultStr
}

//...

extension Bool {

    func getYesNo() -> String {
        if self { return "タップ済みです" }
        else { return "タップしていません" }
    }

}


これらの検査は研究目的のみで実施されるものです!医学的検査を実施できるのは医師のみです。本記事は、このフレームワークの機能の使用法の紹介のみを目的としています。


:relaxed: Twitter @MszPro

:sunny: 私の公開されているQiita記事のリストをカテゴリー別にご覧いただけます。

6
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
5