オープンソースになった ResearchKit の中身を見てみる

昨日、Apple が **ResearchKit フレームワークのソースコードをまるっと GitHub で公開**しました。


- https://github.com/ResearchKit/ResearchKit


ResearchKit とは、


> 医療に携わる科学者が研究のために、必要なデータを集めることができるフレームワーク


というものです。（参考記事： [Appleが全ての医療研究者にResearchKitを開放 | TechCrunch Japan](http://jp.techcrunch.com/2015/04/15/20150414apple-opens-researchkit-to-medical-research-app-developers/)）


Apple の新しいフレームワークのソースコードが公開されるという機会もなかなかないので、中身を見てみました。


##試してみる

何はともあれ何ができるのか体感するためにも、まずは動かしてみたいところ。


さっそくリポジトリから git clone して Xcode で開いてみると・・・


<img src="http://f.st-hatena.com/images/fotolife/s/shu223/20150415/20150415190139.png" width="197">


フレームワークとドキュメント用のSchemeしかない・・・デモは・・・？

iOS Dev Centerを探してもサンプルコードがないので、「自分でつくっていっちょプルリクでも送るか」と思ってつくりはじめて数分後、ふとプロジェクトの別フォルダを見てみたら、既にありました・・・


**ResearchKit.xcodeproj ではなく、samples/ORKCatalog/ORKCatalog.xcodeproj を開く**と、デモアプリをビルドできます。


ただし、"No matching provisioning profiles found" になると思うので、その場合は Xcode で Fix Issue します。（それでも何かエラーになる場合は、iOS Dev Center に行って、<b>該当する App ID について HealthKit が有効になっているか</b>確認する）


ORKCatalog はこんな感じのデモアプリです。


<img src="http://f.st-hatena.com/images/fotolife/s/shu223/20150415/20150415190351.png" width="200">


試しに一番上の "Scale Question" を選択するとこんな画面に遷移しました。

<img src="http://f.st-hatena.com/images/fotolife/s/shu223/20150415/20150415190414.png" width="200">


3番めの "Time of Day Question" を選択するとこんな画面。

<img src="http://f.st-hatena.com/images/fotolife/s/shu223/20150415/20150415190440.png" width="200">


タスクをやり終えて、Resultsタブを選択するとこんな「結果」が表示されました。

<img src="http://f.st-hatena.com/images/fotolife/s/shu223/20150415/20150415190516.png" width="200">


なるほど、ResearchKitというのは、<b>iOSの標準UIコンポーネントによる入力、タッチパネル上でのジェスチャ、加速度センサやGPS、マイク等のセンサ類をうまく使いまわして問診を行い、HealthKitと連携させてデータ収集・管理を行いやすくするフレームワーク</b>、ということでしょうか・・・！？（とりあえず僕はそう解釈しました）


##APIを見てみる

ORKCatalog のソースから、どう ResearchKit を使っているのかみてみました。ちなみに <b>ResearchKit フレームワークは Objective-C で実装されていますが、ORKCatalog は Swift</b> で実装されています。


###ヘッダのインポート、プロトコルへの準拠

```swift:swift
import ResearchKit
```

```swift:swift
class TaskListViewController: UITableViewController, ORKTaskViewControllerDelegate {
```


###セル選択時の処理

※日本語コメントは僕が付け加えたものです。

```swift:swift
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
    tableView.deselectRowAtIndexPath(indexPath, animated: true)

    // 選択されたセルに対応する TaskListRow オブジェクトを取得    
    // Present the task view controller that the user asked for.
    let taskListRow = taskListRows[indexPath.row]
    
    // 取得した TaskListRow オブジェクトにひも付けられた ORKTask を取得
    // Create a task from the `TaskListRow` to present in the `ORKTaskViewController`.
    let task = taskListRow.representedTask
    
    // 取得した ORKTask オブジェクトから、対応する ORKTaskViewController オブジェクトを生成
    /*
        Passing `nil` for the `taskRunUUID` lets the task view controller
        generate an identifier for this run of the task.
    */
    let taskViewController = ORKTaskViewController(task: task, taskRunUUID: nil)

    // ORKTaskViewControllerDelegate をセット
    // Make sure we receive events from `taskViewController`.
    taskViewController.delegate = self
    
    // データを保存するディレクトリをセット
    // Assign a directory to store `taskViewController` output.
    taskViewController.outputDirectory = NSURL(fileURLWithPath: NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true)[0] as! String, isDirectory: true)

    // 遷移する
    /*
        We present the task directly, but it is also possible to use segues.
        The task property of the task view controller can be set any time before
        the task view controller is presented.
    */
    presentViewController(taskViewController, animated: true, completion: nil)
}
```


###ORKTask オブジェクトの生成

上記でセル選択時に TaskListRow を通じて取得している ORKTask オブジェクトは、タスクごとに TaskListRow.swift で生成メソッドが別々に実装されています。


たとえば、上で出てきた「Scale Question」タスクの ORKTask オブジェクトの生成コードはこんな感じです。


```swift:swift
private var scaleQuestionTask: ORKTask {
    var steps = [ORKStep]()
    
    // The first step is a scale control with 10 discrete ticks.
    let step1AnswerFormat = ORKAnswerFormat.scaleAnswerFormatWithMaxValue(10, minValue: 1, step: 1, defaultValue: NSIntegerMax)
    
    let questionStep1 = ORKQuestionStep(identifier: Identifier.DiscreteScaleQuestionStep.rawValue, title: exampleQuestionText, answer: step1AnswerFormat)
    
    questionStep1.text = exampleDetailText
    
    steps += [questionStep1]
    
    // The second step is a scale control that allows continuous movement.
    let step2AnswerFormat = ORKAnswerFormat.continuousScaleAnswerFormatWithMaxValue(5.0, minValue: 1.0, defaultValue: 99.0, maximumFractionDigits: 2)
    
    let questionStep2 = ORKQuestionStep(identifier: Identifier.ContinuousScaleQuestionStep.rawValue, title: exampleQuestionText, answer: step2AnswerFormat)
    
    questionStep2.text = exampleDetailText
    
    steps += [questionStep2]
    
    return ORKOrderedTask(identifier: Identifier.ScaleQuestionTask.rawValue, steps: steps)
}
```


また、"Time of Day Question" の ORKTask オブジェクト生成コードはこんな感じ。

```swift:swift
private var timeOfDayQuestionTask: ORKTask {
    /*
        Because we don't specify a default, the picker will default to the
        time the step is presented. For questions like "What time do you have
        breakfast?", it would make sense to set the default on the answer
        format.
    */
    let answerFormat = ORKAnswerFormat.timeOfDayAnswerFormat()
    
    let questionStep = ORKQuestionStep(identifier: Identifier.TimeOfDayQuestionStep.rawValue, title: exampleQuestionText, answer: answerFormat)
    
    questionStep.text = exampleDetailText
    
    return ORKOrderedTask(identifier: Identifier.TimeOfDayQuestionTask.rawValue, steps: [questionStep])
}
```


- **ORKAnswerFormat で回答のフォーマットを選ぶ**
- **タスクの ORKQuestionStep （タスク内における各質問）を生成**
- **それらを1つ以上指定して ORKOrderedTask を生成**

という手順のようです。


###ORKTaskViewControllerDelegate の実装

タスクが完了すると ORKTaskViewControllerDelegate の `taskViewController:didFinishWithReason:error:` が呼ばれます。


```swift:swift
func taskViewController(taskViewController: ORKTaskViewController, didFinishWithReason reason: ORKTaskViewControllerFinishReason, error: NSError?) {
    /*
        The `reason` passed to this method indicates why the task view
        controller finished: Did the user cancel, save, or actually complete
        the task; or was there an error?

        The actual result of the task is on the `result` property of the task
        view controller.
    */
    taskResultFinishedCompletionHandler?(taskViewController.result)

    taskViewController.dismissViewControllerAnimated(true, completion: nil)
}
```


タスクが完了したのか、保存されたのか、失敗したのか、といった<b>タスク終了の理由</b>が引数に入ってきます。（下記は ORKTaskViewControllerFinishReason の定義）

```swift:swift
enum ORKTaskViewControllerFinishReason : Int {    
    case Saved
    case Discarded
    case Completed
    case Failed
}
```


##フレームワークのソースを見てみる

せっかくオープンソースになったので、フレームワークのソースも見てみました。


###全体像

ResearchKit.h を見てみるとパブリックヘッダだけでもかなり多いので、全体像を把握すべく、objc_dep を使ってクラスの依存関係を可視化してみました。

![](http://f.st-hatena.com/images/fotolife/s/shu223/20150415/20150415190945.png)

- 参考：[Objective-C - Xcodeプロジェクト内のクラスの依存関係を図示する - Qiita](http://qiita.com/shu223/items/170b899df86952544c17)


なんというカオス・・・依存関係図を画像としてexportしたら**横幅12,164ピクセル**にもなってしまいました・・・


ちなみに余談ですが、以前 facebook のアニメーションライブラリ pop のソースを読もうとしてみた際にも依存関係が複雑過ぎると感じたので整理してプルリクを送ったことがありますが、あれを遥かに凌ぐカオスっぷりです。。

- [Facebook Paper のアニメーションエンジン「pop」のソースコードを読んでみる - Over&Out その後](http://d.hatena.ne.jp/shu223/20140502/1399026349)


一部フォルダだけに絞って依存関係を見てみると、相互依存などは少なく、比較的スッキリしていたので、単にとにかくクラスの数が多いためかなと。


###気になったクラス

具体的に ResearchKit の実装のどこを見たい、というのもないので、適当に見てて気になったクラスや実装を挙げていきます。


####ORKHTMLPDFWriter

<b>HTMLを渡すとPDFに変換してそのバイナリデータを出力</b>してくれるクラス。

```objc:objc
- (void)writePDFFromHTML:(NSString *)html withCompletionBlock:(void (^)(NSData *data, NSError *error))completionBlock;
```


実装を見てみると、意外とシンプルで、

```objc:objc
UIPrintFormatter *formatter = self.webView.viewPrintFormatter;
    
ORKHTMLPDFPageRenderer *renderer = [[ORKHTMLPDFPageRenderer alloc] init];

// (中略)
    
[renderer addPrintFormatter:formatter startingAtPageAtIndex:0];
```

UIWebView のプリントフォーマッタを使用、ORKHTMLPDFPageRenderer というクラスでレンダリング。ちなみに ORKHTMLPDFPageRenderer は UIPrintPageRenderer のサブクラス。こんなクラス知らなかったのですが、iOS 4.2 からあるようです。


####ORKAudioContentView

ORKAudioStepViewController で使われる波形表示ビュー。

<img src="http://f.st-hatena.com/images/fotolife/s/shu223/20150415/20150415191035.png" width="200">


最近個人的に波形表示のあるアプリをつくることが多いので、Appleはこう作ってるのかーと。（自分の作り方とだいたい同じだった）


####ORKDataLogger

実装ファイルは 1,600行以上もあって、まだちゃんと見てないのですが、最近いろいろとロガーOSSを見てどれも欲しいのと違って[自作した](http://qiita.com/shu223/items/2ecc4d3684dc8229ae38)ということもあり、ResearchKitのloggerはどこがどう他のloggerと違うのか、それをどう実装しているのか興味あります。


####ORKHelpers

ResearchKit 全体で使用しているマクロやヘルパーメソッドが詰まっています。何か便利なものがあるかもしれません。


####ORKSkin

多くのビュークラスから参照されています。これで全体の見た目を統一しているようです。ORKScreenType で iPhone 4, 5, 6 を分類し、それに応じてソースにベタ書きした数値を返しています。

```
CGFloat ORKGetMetricForScreenType(ORKScreenMetric metric, ORKScreenType screenType) {
    
    static  const CGFloat metrics[ORKScreenMetric_COUNT][ORKScreenType_COUNT] = {
        // iPhone 6,iPhone 5, iPhone 4
        {       128,     100,      100},      // ORKScreenMetricTopToCaptionBaseline
        {        35,      32,       24},      // ORKScreenMetricFontSizeHeadline
        {        38,      32,       28},      // ORKScreenMetricMaxFontSizeHeadline
        {        30,      30,       24},      // ORKScreenMetricFontSizeSurveyHeadline
        {        32,      32,       28},      // ORKScreenMetricMaxFontSizeSurveyHeadline
        {        17,      17,       16},      // ORKScreenMetricFontSizeSubheadline
        // （中略）
    };
    
    return metrics[metric][screenType];
}
```


（つづく・・・）


##所感

僕は医療関係者ではなく、そっち関連の知識もないので、「このフレームワークをつかえばあんなアプリやこんなアプリがつくれる・・・！」というところでのワクワク感はそんなにないのですが、Appleのフレームワークにプルリクを送れて、マージしてもらえる（かも）、というところには非常に可能性を感じます。


実際にプルリクは続々と届いており、コミット履歴を見るとわりと小さい修正でもマージされたりしています。


![](http://f.st-hatena.com/images/fotolife/s/shu223/20150415/20150415192059.png)


README にも Contribution という項目があり、全然 Welcome な雰囲気なので、何か修正したい点を見つけたらリクエストしてみようと思います。