ResearchKit
とは?
ResearchKit
は Apple
が医学研究目的で開発したオープンソースのフレームワークで、多くの機能が搭載されています。ここでは、基本的な医学的検査を例に、このフレームワークの使い方を紹介します。ここで得られる検査結果は公式のものではなく、健康状態を結論づける公式なデータとしては使えないことにご注意ください。
本フレームワークは英語のみで提供されていますが、一部の検査は言語非依存です。
学習内容
- (記憶)空間記憶
- タッピングスピード (iPhone)
- ハノイの塔
- トレイルメイキング(数字と文字を順に結んでいく)
- 視覚コントラスト検査
- 視力検査
- PSAT(前の2つの数字を合計)
- (聴覚)トーン聴覚検査
- (聴覚)dBHLトーン聴覚検査
警告
これらの検査は研究目的のみで実施されるものです!医学的検査を実施できるのは医師のみです。本記事は、このフレームワークの機能の使用法の紹介のみを目的としています。
実装
ステップ1. ResearchKit
フレームワークを複製する
ResearchKit
フレームワークを既存のプロジェクトに複製します: git@github.com:ResearchKit/ResearchKit.git
ステップ2. フレームワークを Xcode
に追加する
次のように、フレームワーク Xcode
のプロジェクトファイルをあなたのプロジェクトファイルにドラッグします:
次に、ResearchKit
のビルドされた製品をプロジェクトターゲットのフレームワーク、ライブラリ、埋め込みコンテンツセクションに追加します:
「+」アイコンをクリックし、 ResearchKit
を選択します。 そして、タイプを Embed & Sign
に変更します
ステップ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` で取り込むことができます
//特定のテストの結果を受け取る方法について説明します。
}
}
各テストの固有コード
(記憶)空間記憶
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)
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
}
}
}
}
//...
ハノイの塔
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
}
}
}
//...
トレイルメイキング(数字と文字を順に結んでいく)
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
}
}
}
//...
視覚コントラスト検査
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)` 総数
}
}
}
//...
視力検査
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つの数字を合計)
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
}
}
}
//...
(聴覚)トーン聴覚検査
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 "タップしていません" }
}
}