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

## `ResearchKit` とは？

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

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

## 学習内容

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

## 警告

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

## 実装

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

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

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

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

<img width="677" alt="Screen Shot 2020-05-21 at 18.41.27.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635330/398d456c-4bf5-bf57-1475-c6f0debb53ce.png">

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

<img width="554" alt="Screen Shot 2020-05-21 at 18.49.15.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635330/a350d8e4-145c-a616-276d-0c0d5a54859f.png">

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

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

```swift
import ResearchKit
```

テストビューを表示：

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

結果を確認するには：

```swift
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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635330/33678061-b1b0-8b63-fb66-6d611a496676.gif)

```swift
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)
```

```swift
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)

<img width="350" alt="picture" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635330/56cf5177-9c82-1bf7-9ba9-c986ea546d3b.gif">

```swift
let task = ORKOrderedTask.twoFingerTappingIntervalTask(withIdentifier: "tappingTest", intendedUseDescription: nil, duration: 10, handOptions: .both, options: [])
//ORKTaskViewController...
```

```swift
//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
            }
        }
    }
}
//...
```

### ハノイの塔

<img width="350" alt="picture" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635330/7e64acc6-b52b-65bb-6093-9cd4752ab8b6.gif">

```swift
let task = ORKOrderedTask.towerOfHanoiTask(withIdentifier: "towerOfHanoi", intendedUseDescription: nil, numberOfDisks: 4, options: [])
//ORKTaskViewController...
```

```swift
//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
        }
    }
}
//...
```

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

<img width="350" alt="picture" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635330/3fe6a64e-1a83-029a-6f2c-2cc6163aa095.gif">

```swift
let task = ORKOrderedTask.trailmakingTask(withIdentifier: "trailMaking", intendedUseDescription: nil, trailmakingInstruction: nil, trailType: .B, options: [])
//ORKTaskViewController...
```

```swift
//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
        }
    }
}
//...
```

### 視覚コントラスト検査

<img width="350" alt="picture" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635330/278dbfac-c2d3-0132-e381-5de6befe07b1.gif">

```swift
let task = ORKOrderedTask.landoltCContrastSensitivityTask(withIdentifier: "landoltCcontrast", intendedUseDescription: nil)
//ORKTaskViewController...
```

```swift
//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)` 総数
        }
    }
}
//...
```

### 視力検査

<img width="350" alt="picture" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635330/90ba83b4-8126-9c46-0e6f-4ff5e8d37918.gif">

```swift
let task = ORKOrderedTask.landoltCVisualAcuityTask(withIdentifier: "visualAcuity", intendedUseDescription: nil)
//ORKTaskViewController...
```

```swift
//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つの数字を合計）

<img width="350" alt="picture" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635330/95c27e35-9af9-d132-75e6-d58454867e59.gif">

```swift
let task = ORKOrderedTask.psatTask(withIdentifier: "psat", intendedUseDescription: nil, presentationMode: .visual, interStimulusInterval: 4.5, stimulusDuration: 4.5, seriesLength: 12, options: [])
//ORKTaskViewController...
```

```swift
//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
        }
    }
}
//...
```

### （聴覚）トーン聴覚検査

<img width="350" alt="picture" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635330/952ba3ac-d26a-9761-8ed8-4a5257f0be13.jpeg">

Present the test view:

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

Checking the result

```swift
//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トーン聴覚検査

```swift
let task = ORKOrderedTask.dBHLToneAudiometryTask(withIdentifier: "dBHLToneAudiometry", intendedUseDescription: nil, options: [])
//ORKTaskViewController...
```

```swift
//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](https://twitter.com/MszPro)

:sunny: [私の公開されているQiita記事のリストをカテゴリー別にご覧いただけます。](http://mszpro.com/ioskiji/)
<img width="150" src="https://cdn.mszmagic.com/static-web-content/AppClipImage_small.png">