iOSでグラフ表示

iOS始めました

このところ、仕事でメインの広告関連システム開発ディレクションとは別に、iOS開発でのディレクションも行っている。プライベートでも、週末に通っている大学院でiOSアプリの開発をしていて、大分iOSに浸かった日々を送っている。
今回はその中でグラフを表示するiOSのライブラリを使ってみたので備忘録として残しておく。

BEMSimpleLineGraph

使ったのはこれ。github.com
グラフ表示に使えるライブラリはいくつかあったものの、開発がアクティブで手軽に使えそうなものという観点でこれを選んだ。

今からどうせやるならSwiftということで、以下を参考にしながら使ってみた。d.hatena.ne.jp
また下記を参考に、cocoapodsからライブラリをインストールできるようにした。
CocoaPodsを使う | e.lab

まずはプロジェクトディレクトリのトップにPodfileを作成し、pod install実行。

$ cat Podfile
pod 'BEMSimpleLineGraph'
$ pod install
Updating local specs repositories
Analyzing dependencies
Downloading dependencies
Installing BEMSimpleLineGraph (4.1)
Generating Pods project
Integrating client project
Sending stats
Pod installation complete! There is 1 dependency from the Podfile and 1 total pod installed.

cocoapodsでインストールしたライブラリを使うには.xcworkspaceから起動する必要があるということで下記で起動。

$ open <APP名>.xcworkspace

BEMSimpleLineGraphはObjective−Cのライブラリなので、Swiftで使う場合は連携させるためのファイル(<APP名>-Bridging-Header.h)を作成する必要がある。
最近自分が作っている万歩計アプリ(QuestWalk)でいうとこんな感じ。
QuestWalk-Bridging-Header.hという名前でヘッダファイルを作成し、BEMSimpleLineGraphView.hのimport文を記載する。
f:id:shinjukujohnny:20151109012611p:plain
プロジェクトのBuildSettingsを開いてSwift Compiler - Code GenerationのObjective-C Bridging HeaderにBridging Headerのパスを記載。
f:id:shinjukujohnny:20151109013019p:plain
これで使う準備は整った。
自分の場合は、CMPedometerで一週間分の歩数データを取得して、それをグラフに書き出すというのをやってみた。実装はViewControllerに下記のようにした。(一週間分の歩数データが取得できない場合は、画面に"No Data"と表示される。BEMSimpleLineGraphの仕様?)

//
//  QWStatusGraphViewController.swift
//  QuestWalk
//
//  Created by Johnny on 11/1/15.
//  Copyright © 2015 SBR2015. All rights reserved.
//

import UIKit
import Foundation
import CoreMotion

// BEMSimpleLineGraphDelegateとBEMSimpleLineGraphDataSourceの2つのプロトコルを追加
class QWStatusGraphViewController: UIViewController, BEMSimpleLineGraphDelegate, BEMSimpleLineGraphDataSource {
    
    // メンバー変数でないと動作しないので注意
    let pedometer = CMPedometer()
    var weekList:Array<Float>!

    required init(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)!
        setup()
    }
    
    override init(nibName nibNameOrNil: String!, bundle nibBundleOrNil: NSBundle!) {
        super.init(nibName: nil, bundle: nil)
        setup()
    }
    
    convenience init() {
        self.init(nibName: nil, bundle: nil)
    }
    
    func setup() {
        // init の中身
        getWeekData()
    }
    override func viewDidLoad() {
        super.viewDidLoad()
        // 描画の方が早いのでタイマーで遅延させる
        NSTimer.scheduledTimerWithTimeInterval(1, target: self, selector: "onUpdate:", userInfo: nil, repeats: false)
    }
    
    func onUpdate(timer:NSTimer) {
        // グラフのViewを作成(今回はメインビューと同じ大きさのビューを作ります)
        let graphView: BEMSimpleLineGraphView = BEMSimpleLineGraphView(frame: CGRectMake(0, 50, self.view.bounds.width, (self.view.bounds.height - 50)))
        // データソースを設定 (今回はこのクラスの中にメソッドを書くので、selfを設定)
        graphView.dataSource = self
        // delegateを設定 (今回はこのクラスの中にメソッドを書くので、selfを設定)
        graphView.delegate = self
        graphView.enableTouchReport = true
        graphView.enablePopUpReport = true
        graphView.enableYAxisLabel = true
        graphView.enableXAxisLabel = true
        graphView.enableReferenceAxisFrame = true
        graphView.enableReferenceXAxisLines = true
        graphView.enableReferenceYAxisLines = true
        graphView.enableBezierCurve = false
        graphView.widthLine = 2
        graphView.colorLine = UIColor.orangeColor()
        graphView.colorTop = UIColor.whiteColor()
        graphView.colorBottom = UIColor.whiteColor()
        // メインビューにグラフのViewを追加
        self.view.addSubview(graphView)
    }
    
    // Y軸の値を返すメソッドを作成
    func lineGraph(graph: BEMSimpleLineGraphView, valueForPointAtIndex index: NSInteger) -> CGFloat {
        //何個目のX軸のポイントかはindexで取得できるので、今回はSampleData配列の中にあるindexの要素をそのまま返します
        return CGFloat(self.weekList[index])
    }
    
    func numberOfPointsInLineGraph(graph: BEMSimpleLineGraphView) -> NSInteger {
        return self.weekList.count
    }
    
    func getWeekData() {
        weekList = Array<Float>()
        for i in 0...6 {
            // CMPedometerが利用できるか確認
            if CMPedometer.isStepCountingAvailable() {
                // 今日の0時
                let df = NSDateFormatter()
                df.locale = NSLocale(localeIdentifier: "ja_JP")
                df.dateFormat = "yyyy/MM/dd 00:00:00"
                let fromDate = df.dateFromString(df.stringFromDate(NSDate(timeIntervalSinceNow: Double(-24*60*60*(i+1)))))
                print("###from date### " + df.stringFromDate(NSDate(timeIntervalSinceNow: Double(-24*60*60*(i+1)))))
                // 現在時刻
                let toDate = df.dateFromString(df.stringFromDate(NSDate(timeIntervalSinceNow: Double(-24*60*60*i))))
                print("###to date### " + df.stringFromDate(NSDate(timeIntervalSinceNow: Double(-24*60*60*i))))
                
                // 本日の成果
                pedometer.queryPedometerDataFromDate(fromDate!, toDate: toDate!, withHandler: {
                    [unowned self] data, error in
                    dispatch_sync(dispatch_get_main_queue(), {
                        if error != nil {
                            print("エラー : \(error)")
                        } else {
                            //let lengthFormatter = NSLengthFormatter()
                            // 歩数
                            let steps = data!.numberOfSteps
                            // 距離
                            //let distance = data!.distance!.doubleValue
                            print("Steps: \(steps)")
                            //    + "\n\nDistance : \(lengthFormatter.stringFromMeters(distance))"
                            self.weekList.insert(steps.floatValue, atIndex: 0)
                        }
                    })
                })
            } else {
                print("Cannot use CMPedometer")
            }
        }
    }
    
    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }
    
    /*
    // MARK: - Navigation
    
    // In a storyboard-based application, you will often want to do a little preparation before navigation
    override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
    // Get the new view controller using segue.destinationViewController.
    // Pass the selected object to the new view controller.
    }
    */
    
}

結果は下記の通り。
f:id:shinjukujohnny:20151109014325p:plain
一点、参考にしたサイトの下記の実装(X軸のラベル表示)は自分が試した際には動かなかった。よく調べきれてないが、Objective-CのライブラリをSwiftから全く同じように使うことはできないのかもしれない。

    // X軸のラベルを返すメソッドを作成
    func lineGraph(graph: BEMSimpleLineGraphView, labelOnXAxisForIndex index: NSInteger) -> NSString {
        //何個目のX軸のポイントかはindexで取得できるので、今回はSampleLabel配列の中にあるindexの要素をそのまま返します
        return NSString(string: SampleLabel[index])
    }

また今回の実装ではCMPedometerから歩数を抽出している間に、先にViewにグラフが描画されてしまいデータが渡らない現象が起きていた。デバッグしたところ、dispatch_sync内で歩数データを配列に入れる処理がViewの描画の後になってしまっているようだった。苦肉の策としてviewDidLoadメソッド内でタイマーを設定して、Viewにグラフを描画するのを遅らせることで対応したが、もっと良い方法があるような気がしている。
この辺の同期・非同期処理の順番やiOSのライフサイクルなどは引き続き調べてみたいと思いました。