その後のその後

iOSエンジニア 堤 修一のブログ github.com/shu223

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

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


ここで「おお ResearchKit!!・・・って何だっけ・・・?」ってなった方も実は多いのではないでしょうか。僕はすっかり忘れててググって思い出したのですが、

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


というやつです *1


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

試してみる

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


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



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


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


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


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


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




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




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




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




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

APIを見てみる

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

ヘッダのインポート、プロトコルへの準拠
import ResearchKit
class TaskListViewController: UITableViewController, ORKTaskViewControllerDelegate {
セル選択時の処理

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

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 オブジェクトの生成コードはこんな感じです。

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 オブジェクト生成コードはこんな感じ。

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:` が呼ばれます。

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)
}


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

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

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

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

全体像

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




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


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


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

気になったクラス

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

ORKHTMLPDFWriter

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

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


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

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

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

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

ORKAudioContentView

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



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

ORKDataLogger

実装ファイルは 1,600行以上もあって、まだちゃんと見てないのですが、最近いろいろとロガーOSSを見てどれも欲しいのと違って自作したということもあり、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のフレームワークにプルリクを送れて、マージしてもらえる(かも)、というところには非常に可能性を感じます。


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



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



ResearchKitを使った医療関連アプリのお仕事もお待ちしております!