iOS
canvas
Swift
Realm
Swift2.0

カロリー記録サンプルアプリで見る実装ポイントまとめ

More than 1 year has passed since last update.

はじめに. まえがきと自己紹介

年の瀬ということもあり、仕事プライベート問わず忙しい時期に皆様いかがおすごしでしょうか?

はじめまして、12/22のAdventCalendarを担当するfumiyasacです。(本業は一応PHP/Rubyがメインですが、好きなのはSwift/Objective-Cです笑)

今回AdventCalendarにチャレンジしてみるに当たって、まだまだ個人的に7ヶ月程度しか経験のない私ではありますが宜しくお願いします。

最近は忘年会シーズンということもあり食べ過ぎてしまうことが多いということもありまして、自戒の意味も込めまして下記のようなサンプルを作成しました。

今回のサンプル:私のカロリーグラフ(試作品)

今回は上記の様な割と本格的?なデータ記録系アプリを作成するにあたっての個人的に感じたことや実装のポイントをオムニバス形式で解説できればと思っています。

1つのテーマに絞っているわけではないので、内容の脈絡はない感じですがご参考になれば幸いです。
※上記サンプルに関する要望や不具合等に関するご意見等あればお気軽にお願い致しますm(_ _)m

0. 今回のサンプルの見た目&おおまかな仕様

★使用環境

OS:Yosemite 10.10.5 ※まだEl Capitanにはしていません><
XCode:XCode 7.2
Swift:Swift2.1.1

★大まかな仕様や流れに関して

今回のサンプルは自分が食べたもののカロリーを記録するだけのアプリですが、下記のような仕様を想定して作成してみました。

calorie_graph.png

1.データの追加&削除

  • ID (プライマリキー) [Int型]
  • 食べたもの [String型]
  • 推定カロリー [Int型]
  • 食べた日にち [NSDate型]
  • 画像 [NSData?型]

2.登録されている記録の一覧表示

  • UITableViewで登録されたデータが食べた日にち順に上から並ぶ
  • セルをスワイプすると削除ボタンが表示される
  • 削除ボタンを押すと該当データが削除されデータとグラフが更新される

3.登録されているデータをグラフで表示

  • カロリーの高い順Best3(棒グラフ)
  • 直近3つのデータを表示したカロリー推移(折れ線グラフ)

また、今回はより実際にリリースされている様なアプリを作るノリでサンプルを作ってみたつもりですので細かな部分に関しても解説を加えています。Githubにて公開しているソースについてもコメントを多めにつけてみました。

※サンプルはAutoLayoutを使用しています。

1. データの永続化にはRealmを使用

導入にあたってはCocoaPodsでRealmSwiftを導入した&Realm初めてだったので少し苦労をしてしまいましたが、下記の参考資料を元に作成しました。

今まで僭越ながらも2本ほどiPhoneアプリを出しており、その際にCoreDataを使用してデータの記録処理を実装していましたが、最近ではRealmの人気や知名度が上がってきているということもあり、せっかくの機会なので使用してみたいと思いました。

★1-1. RealmSwift導入&今回のサンプルを作るにあたりインスパイアされた記事

・参考記事その1:【初心者向け】徹底詳解!cocoapod + RealmでToDoアプリを作るチュートリアル (全4回)
・参考記事その2:【初心者向け】徹底詳解!cocoapod + RealmでToDoアプリを作るチュートリアル (全4回)
・参考記事その3:【初心者向け】徹底詳解!cocoapod + RealmでToDoアプリを作るチュートリアル (全4回)
・参考記事その4:【初心者向け】徹底詳解!cocoapod + RealmでToDoアプリを作るチュートリアル (全4回)

導入までの手順が詳細に記載されている上記の記事のおかげで導入もスムーズにすることができました。また今回のサンプルを作る上での参考にしましたので非常に助かりました。

★1-2. 使用してみた個人的な雑感

特にCoreDataではできなかったプライマリキーが使える部分は個人的にポイントが高い部分でした。また、Modelクラスにテーブルのスキーマやメソッドを記述して使う感じが、その他のRailsやCakePHPのModelっぽくていいなと感じた次第です。

★1-3. RealmSwiftの継承関係と今回のスキーマ定義

下記にざっくりとではありますがクラスの継承関係を書いておきます。
RealmSwiftを使用しているのですが、RealmSwiftの中をのぞいて見ると下記のようなクラスがあります。

Object.swift(RealmSwift内)
public class Object: RLMObjectBase {
----- (中は省略) -----
}

このObjectクラスを継承して各々のデータ登録するためのModelクラスを作成していく感じになります。このクラス内でサンプルのスキーマの定義を記載していきます。

Calorie.swift
Calorie : Object {

    //Realmクラスのインスタンス
    static let realm = try! Realm()

    //id
    dynamic private var id = 0

    //今食べたもの
    dynamic var food = ""

    //推定カロリー
    dynamic var amount = 0

    //食べた日にち
    dynamic var eatDate = NSDate(timeIntervalSince1970: 0)

    //その時の写真
    /**
     * 下記記事で紹介されている実装を元に作成
     *
     * JFYI:(Qiita) Realm × Swift2 でシームレスに画像を保存する
     * http://qiita.com/_ha1f/items/593ca4f9c97ae697fc75
     *
     */
    //
    dynamic private var _image: UIImage? = nil

    dynamic var image: UIImage? {

        //setter
        set{
            self._image = newValue
            if let value = newValue {
                self.imageData = UIImagePNGRepresentation(value)
            }
        }

        //getter
        get{
            if let image = self._image {
                return image
            }
            if let data = self.imageData {
                self._image = UIImage(data: data)
                return self._image
            }
            return nil
        }
    }

    dynamic private var imageData: NSData? = nil

    //PrimaryKeyの設定
    override static func primaryKey() -> String? {
        return "id"
    }
    ----- (省略) -----
}

というイメージになります。

★JFYI:Realmについての概要を知る

Realmを使ってデータ管理【Swift編】-その1-
Realmを使ってデータ管理【Swift編】-その2-
Realmを使ってデータ管理【Swift編】-その3-
RealmSwiftを試したけどもうCoreDataは使わなくなるかもしれない
Realm for Swift まとめ完全版

2. 写真に関する処理の総まとめ

ただ文字データを保存するだけでは少しそっけないかな?なんて思ったので少しリッチに画像データをRealmに保存できるようにしました。

この部分での実装のポイントは下記の2点になります。

★2-1. カメラアクセスに関する部分

今回のサンプルでは「カメラで撮影した or ライブラリから選択した画像も一緒に保存できる」ようにしてあります。

カメラや写真の呼び出しの部分はもう既にお馴染みなのかもしれませんが、今回サンプルでのカメラを使用した部分は下記になります。

AddController.swift
//※必要なデリゲートは下記になります。
//テキストフィールド:UITextFieldDelegate
//カメラ及びライブラリへのアクセス:UIImagePickerControllerDelegate,UINavigationControllerDelegate

//ライブラリから写真を選択してimageに書き出す
func selectAndDisplayFromPhotoLibrary() {

    //フォトアルバムを表示
    let ipc = UIImagePickerController()
    ipc.delegate = self
    ipc.sourceType = UIImagePickerControllerSourceType.PhotoLibrary
    presentViewController(ipc, animated: true, completion: nil)
}

//カメラで撮影してimageに書き出す
func loadAndDisplayFromCamera() {

    //カメラを起動
    let ip = UIImagePickerController()
    ip.delegate = self
    ip.sourceType = UIImagePickerControllerSourceType.Camera
    presentViewController(ip, animated: true, completion: nil)
}

//画像を選択した時のイベント
//※このメソッドはカメラで撮影した際もライブラリで選択した際も呼ばれます。
func imagePickerController(picker: UIImagePickerController, didFinishPickingImage image: UIImage, editingInfo: [String : AnyObject]?) {

    //画像をセットして戻る
    self.dismissViewControllerAnimated(true, completion: nil)

    //リサイズして表示する
    let resizedImage = CGRectMake(
        image.size.width / 4.0,
        image.size.height / 4.0,
        image.size.width / 2.0,
        image.size.height / 2.0
    )
    let cgImage = CGImageCreateWithImageInRect(image.CGImage, resizedImage)
    self.foodPicture.image = UIImage(CGImage: cgImage!)
    //self.foodPicture.image = image
}
★2-2. Realmに保存するためにデータの型を変換する

画像データを保存する場合UIImage?データをNSData型に変換する必要があります。この部分に関しては下記の記事を参考に実装しました。

参考記事:Realm × Swift2 でシームレスに画像を保存する

※下記の記事も参考になりました!

参考記事:Realm × Swift2 でidでデータを管理する
参考記事:Realm × Swift2 でデータを保存してみる

僕自身、型変換の部分は比較的型のLight Langageばかり触っていたので、たまに予期しないところでハマってしまう時もありますので注意しなければ...と感じた次第です。

3. テキストフィールドやデイトピッカーの制御

今回は入力フォームにUITextFieldを使用しているのですが、その際にテキストフィールドに関して自分なりに気をつけてみた点をピックアップしてみました。

各UITextFieldの振る舞いに関しては、

  • 今食べたもの → 標準のキーボード表示
  • 推定カロリー → 数字だけのキーボード表示
  • 食べた日にち → キーボードではなくてデイトピッカーを表示

という形にしようと思いました。

また上記の要件を満たすための実装のポイントとして、

  1. 入力完了時にキーボードが出っ放しの状態を防ぐ
  2. 入力例のplaceholderとキーボードタイプについて
  3. 任意のテキストフィールドが選択された際に他のテキストフィールドを選択不可にする
  4. デイトピッカー表示をするテキストフィールドではキーボードを出さなくする
  5. 日付部分のNSDate型ないしはString型への相互変換

5点に関してピックアップしていきます。

★3-1. キーボードが出っ放しの状態を防ぐ

UITextFieldをタップした際はキーボードが表示されるのですが、入力を完了したとしてもキーボードは自動で隠れてくれません。

そのため、現在入力中のテキストフィールドの外をタップした際にTapGestureRecognizerを利用してキーボードを隠す処理を追加します。

AddController.swift
 //背景をタップしてキーボードを引っ込めるアクション
 @IBAction func hideKeyboardAction(sender: UITapGestureRecognizer) {

    //表示キーワード
    self.view.endEditing(true)

    //デイトピッカーを隠す
    self.hideDatePicker()

    //ハイライトやボタン状態を元に戻す
    self.resetTextFieldStatus()
}

一番親のviewにTapGestureを登録して、self.view.endEditing(true)とすると実現できます。
※このサンプルではキーボードだけでなくテキストフィールドを選択した際に他のボタンやデイトピッカーも元に戻す様にしています。

★3-2. 入力例のplaceholderとキーボードタイプについて

Webサイト等でもフォームの入力例や数字のみの入力欄では数字のみしか入力できない等、予期しない入力をできるだけ少なくするための工夫がされているものは最近では増えてきています。

こちらはUITexitFieldのプロパティの値を設定するだけで簡単に実現できます。

AddController.swift
//例:推定カロリーの入力欄

//UITextFieldのプレースホルダーを設定
self.calorieAmountField.placeholder = "(例)730"

//数字入力のみのキーボードにする
self.calorieAmountField.keyboardType = UIKeyboardType.NumberPad

UITextFieldのプロパティに関しては、まだまだ沢山のパターンがありますので深掘りしてみると面白い発見やさらに細やかな設定をする上で役に立つかと思います。

★3-3. 任意のテキストフィールドが選択された際に他のテキストフィールドを選択不可&写真選択ボタンを非活性にする

こちらは★3-4. と関連する部分ですのでそちらで一緒に解説します。

★3-4. デイトピッカー表示をするテキストフィールドではキーボードを出さなくする

現在編集中のテキストフィールドとは別のテキストフィールドをタップした際は通常はそのまま続けて入力ができるのですが、今回はキーボードではなくデイトピッカーを表示して日付をyyyy/MM/ddの形式で表示した状態で入力できるようにしたいと思いました。

chapter3_textfield.png

その中で日付のテキストフィールドをタップした際に、デイトピッカーは問題なく表示できたのですが、一緒にキーボードまで出てきてしまう状態になってしまいました。(意外とこの部分はハマってしまいました...)

そこで今回の方針としては、UITextFieldはtagプロパティを用いてどのテキストフィールドかを識別できるので、これを活用しての制御を行うことにしました。

なおテキストフィールドの編集が開始されたことを検知するのは func textFieldShouldBeginEditing(textField: UITextField) -> Bool で可能です。falseの場合にはテキストフィールドが編集されていないと認識されるので、デイトピッカーのテキストフィールドがタップされた場合には戻り値がfalseとなるようにします。

少し冗長な書き方かもですが、コードは下記のようになります。

AddController.swift
//UITextFieldを識別するためのタグ
self.foodNameField.tag = TextFieldIdentifier.InputFood.returnValue()
self.calorieAmountField.tag = TextFieldIdentifier.InputCalorie.returnValue()
self.eatDateField.tag = TextFieldIdentifier.InputDate.returnValue()

//各テキストフィールドに対してデリゲートを設定
self.foodNameField.delegate = self
self.calorieAmountField.delegate = self
self.eatDateField.delegate = self

 ----- (省略) -----

//UITextFieldを識別するためのタグキーボードが重なってしまうのでUITextFieldDelegateを使う
func textFieldShouldBeginEditing(textField: UITextField) -> Bool {

    //カメラボタンを無効にする
    self.foodPictureBtn.enabled = false

    //「食べた日にち」のテキストフィールドタップ時のアクションのテキストフィールドがタップされた際はデイトピッカーを表示する
    if textField.tag == TextFieldIdentifier.InputDate.returnValue() {

        self.foodNameField.enabled = false
        self.calorieAmountField.enabled = false
        self.eatDateField.enabled = true
        self.showDatePicker()
        return false

    } else {

        //「今食べたもの」のテキストフィールドタップ時のアクション
        if textField.tag == TextFieldIdentifier.InputCalorie.returnValue() {

            self.foodNameField.enabled = false
            self.calorieAmountField.enabled = true
            self.eatDateField.enabled = false
            self.hideDatePicker()

        //「推定カロリー」のテキストフィールドタップ時のアクション
        } else if textField.tag == TextFieldIdentifier.InputFood.returnValue() {

            self.foodNameField.enabled = true
            self.calorieAmountField.enabled = false
            self.eatDateField.enabled = false
            self.hideDatePicker()
        }
        return true
    }

}

ユーザーの入力する部分に関してはこの仕様だとまだまだ甘い部分がきっとありそうな気がするので、もしお気づきの点や「もっとこうした方がスマートだよ」というご指摘があれば遠慮なくお願い致します。

★3-5. 日付部分のNSDate型ないしはString型への相互変換
  • 表示時 → yyyy/MM/ddの文字列型
  • Realm登録時 → NSDate型

の相互変換をするために下記のようなstructを定義して日付の型変換処理を使いまわすことができるようにしました。

ChangeNSDateOrString.swift
//日付をNSDateまたはStringの相互変換用のstruct
struct ChangeNSDateOrString {

    //NSDate → Stringへの変換
    static func convertNSDateToString (date: NSDate) -> String {

        let dateFormatter: NSDateFormatter = NSDateFormatter()
        dateFormatter.locale = NSLocale(localeIdentifier: "ja_JP")
        dateFormatter.dateFormat = "yyyy/MM/dd"
        let dateString: String = dateFormatter.stringFromDate(date)
        return dateString
    }

    //String → NSDateへの変換
    static func convertStringToNSDate (dateString: String!) -> NSDate {

        if dateString != nil {
            let dateFormatter: NSDateFormatter = NSDateFormatter()
            dateFormatter.locale = NSLocale(localeIdentifier: "ja")
            dateFormatter.dateFormat = "yyyy/MM/dd"
            let stringDate: NSDate = dateFormatter.dateFromString(dateString)!
            return stringDate
        } else {
            let date = NSDate()
            return date
        }
    }

}

上記のようにDB等に登録するデータと表示するデータの型が違うということは結構よくあります。その他にも使いまわす局面が多いような処理はできるだけstructやclass化して再利用できるようにすると良いですね。

ユーザーがデータを入力する画面は、開発者とユーザーのコミュニケーションが生まれる大事な部分なので、実際のアプリでもしっかりとこだわりたい部分だと改めて実感した次第です。

4. テーブルビューの基本的なカスタマイズ

テーブルビューに関しては、このサンプルでは入力したカロリーデータを一覧表示をするために使用しています。
(テーブルビューのセルに関してはXibファイルに切り出して使用しています)
今回の使用はシンプルに追加と削除ができるだけですが、間違ったデータを入力した場合には、

  1. スワイプすると削除ボタンを表示する
  2. 削除ボタンを押すとRealmから該当データが1件削除されてテーブルビューの表示内容・グラフの表示内容が更新される

という流れを想定しています。
(メールクライアントアプリとかである動きに近い形のやつです)

★4-1. スワイプして削除ボタン表示 → データ削除&テーブルビューとグラフのリロード

先ほど紹介した上記2つの処理を実現するためには

  • 対象のセルに対してEditableの状態&ボタン操作の有効にする
  • 削除ボタンを作成し押された際にはデータが1件削除する
  • テーブルビューと表示しているグラフデータを更新する

の3つの処理がポイントとなります。

ViewController.swift
//TableView: Editableの状態にする.
func tableView(tableView: UITableView, canEditRowAtIndexPath indexPath: NSIndexPath) -> Bool {
    return true
}

//TableView: 特定の行のボタン操作を有効にする.
func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {
    print("commitEdittingStyle:\(editingStyle)")
}

//TableView: Buttonを拡張&データ削除処理
func tableView(tableView: UITableView, editActionsForRowAtIndexPath indexPath: NSIndexPath) -> [UITableViewRowAction]? {

    //削除ボタン
    let myDeleteButton: UITableViewRowAction = UITableViewRowAction(style: .Normal, title: "削除") { (action, index) -> Void in

        //テーブルビューを編集不可にする
        tableView.editing = false

        //データを1件削除(この処理に関してはCalorie.swiftファイルを参照して下さい)
        let calorieData: Calorie = self.caloriesArrayForCell[indexPath.row] as! Calorie
        calorieData.delete()

        //データをリロードする
        self.fetchAndReloadData()    
    }
    myDeleteButton.backgroundColor = UIColor.redColor()
    return [myDeleteButton]
}

テーブルビューを使用する場面はものすごく多いですし、私も個人アプリを開発する上ではUITableViewは欠かせないものとなっています。TableViewの特性をしっかりと押さえると様々な工夫ができてUIの作成がとても楽しくなりますよ^^

5. ローカルのHTML / JavaScriptを利用したグラフ描画

iOSアプリ開発をする際に、グラフに関してはCorePlotを使って表示する場合が多いのですが、今回はなるべく手軽に表示をしたいということもあり、WebViewを活用した簡易的なグラフ機能を利用しています。

今回使用しているグラフ機能のポイントとしては、

  1. ローカルのHTMLファイルを読み出す
  2. JavaScriptのグラフ描画のライブラリであるChart.jsを使用
  3. Chart.jsのデータの記述に合わせてアプリ側で表示データを作成
  4. アプリ側からJavaScriptの関数を実行する

の4つになります。今回のサンプルでは

  • ハイカロリーBest3(棒グラフ)
  • カロリー推移(折れ線グラフ)

の2種類を用意していますが、今回はハイカロリーBest3の方に焦点を当てて解説をしていこうと思います。

★5-1. ローカルのHTMLファイルを読み出す

今回はプロジェクト内に取り込んでいるHTMLファイルをUIWebViewで読み出してグラフを表示するような仕様で作成していますのでアプリ側でWebViewを読み出す処理を書きます。

ViewController.swift
//ローカルのhtmlファイルを読み込む
func loadLocalHtmlSource(status: GraphStatus) {

    //htmlファイルへアクセスする
    do {

        //ローカルアクセス用のパスを格納する変数
        var path: String = ""

        //棒グラフ
        if status == GraphStatus.BarGraph {

            path = NSBundle.mainBundle().pathForResource("barchart", ofType: "html")!

        //折れ線グラフ
        } else if status == GraphStatus.LineGraph {

            path = NSBundle.mainBundle().pathForResource("linechart", ofType: "html")!

        }

        let htmlFile = try String(contentsOfFile: path, encoding: NSUTF8StringEncoding)
        self.grachWebView.loadHTMLString(htmlFile, baseURL: NSURL(fileURLWithPath: NSBundle.mainBundle().bundlePath))
        self.grachWebView.scalesPageToFit = false

    } catch _ as NSError {

    }

}
★5-2. JavaScriptのグラフ描画のライブラリであるChart.jsを使用

そしてグラフ用のHTMLファイルを用意します。(html内にCSSやJavaScriptを記述しています)
HTMLに書いてあるJavaScriptではChart.jsのグラフ描画処理を関数にラッピングしています。

※Chart.jsのグラフの記述方法につきましては、下記の公式ドキュメントを参照して下さい。
※この記事で参考にするのはBar Chartの部分になります。
Chart.js | Documentation

barchart.html
<!doctype html>
<html>
<head>
<title>カロリーBest3</title>
<style>
    #wrapper{
        margin: 0 auto;
    }

    #mycanvas{
        width: 320px;
        height: 230px;
        color: #ffffff;
    }
</style>
</head>
<body>
<script src="./Chart.js"></script>
<script>
function drawBarChart(expense, sum){
    barChartData = {
        labels : [expense[0],expense[1],expense[2]],
        datasets : [
            {
                label: "ハイカロリーBest3",
                fillColor : "rgba(220,220,220,0.5)",
                strokeColor : "rgba(220,220,220,0.8)",
                highlightFill: "rgba(220,220,220,0.75)",
                highlightStroke: "rgba(220,220,220,1)",
                data :[sum[0],sum[1],sum[2]]
            }
        ]
    }

    //アニメーション処理を記述
    ctx = document.getElementById("mycanvas").getContext("2d");
    window.myBar = new Chart(ctx).Bar(barChartData,{responsive : true});
}
</script>
<div id="wrapper">
<canvas id="mycanvas"></canvas>
</div>
</body>
</html>

HTML/CSS/JavaScriptの書き方は若干雑ですが、これでグラフ表示用の仕込みができました!次にJavaScriptの関数の引数に入る部分をアプリ側で作成

★5-3. Chart.jsのデータの記述に合わせてアプリ側で表示データを作成

こちらは★5-4. と関連する部分ですのでそちらで一緒に解説します。

★5-4. アプリ側からJavaScriptの関数を実行する

前のHTML内に書いたJavaScript書いたdrawBarChart関数の引数に渡すものを、アプリ側で実装していくのですが、実行したいJavaScriptの関数を文字列として記述する必要があります。そしてその関数を実行するためには stringByEvaluatingJavaScriptFromString メソッドを使用します。

ViewController.swift
//グラフ用の可変配列をメンバ変数へ設定
var caloriesArrayForBarChart: NSMutableArray = []

 ----- (省略) -----

//棒グラフ用の可変配列の作成 (viewWillAppear内のfetchAndReloadDataメソッド内の処理)
self.caloriesArrayForBarChart.removeAllObjects()
let caloriesForBarChart = Calorie.fetchAllCalorieListSortByAmount() //Calorie.swift内を参照
if caloriesForBarChart.count != 0 {
    for calorieForBarChart in caloriesForBarChart {
         self.caloriesArrayForBarChart.addObject(calorieForBarChart)
    }
}

 ----- (省略) -----

//棒グラフ用のデータを作成してWebViewへ描画(実装汚くてすみません...)
func displayBarGraphToWebView() {

    if self.caloriesArrayForBarChart.count > 0 {

    //棒グラフ用の文字列を生成する
    var initBarChart: String

        if self.caloriesArrayForBarChart.count == 1 {

            let first: Calorie = self.caloriesArrayForBarChart[0] as! Calorie        

            initBarChart = NSString(format:"drawBarChart(['%@','%@','%@'],[%@,%@,%@]);", first.food, "なし", "なし", String(first.amount), "0", "0") as String

        } else if self.caloriesArrayForBarChart.count == 2 {

            let first: Calorie = self.caloriesArrayForBarChart[0] as! Calorie
            let second: Calorie = self.caloriesArrayForBarChart[1] as! Calorie

            initBarChart = NSString(format:"drawBarChart(['%@','%@','%@'],[%@,%@,%@]);", first.food, second.food, "なし", String(first.amount), String(second.amount), "0") as String

        } else {

            let first: Calorie = self.caloriesArrayForBarChart[0] as! Calorie
            let second: Calorie = self.caloriesArrayForBarChart[1] as! Calorie
            let third: Calorie = self.caloriesArrayForBarChart[2] as! Calorie

            initBarChart = NSString(format:"drawBarChart(['%@','%@','%@'],[%@,%@,%@]);", first.food, second.food, third.food, String(first.amount), String(second.amount), String(third.amount)) as String

        }
        //ローカルWebviewのJavaScriptのメソッドをキックする
        self.grachWebView.stringByEvaluatingJavaScriptFromString(initBarChart)
    }

}

グラフの描画までの流れとしては、

  1. アプリ側でデータを取得して文字列としてJavaScriptの関数を作成
  2. 文字列のJavaScriptの関数を実行
  3. ローカルのWebViewの表示

というかなりトリッキーな実装にはなっています。

※ 余談ですがなぜこんな方法を思いついたのか自分自身も未だに謎です...

★補足1:ChartJSとローカルWebViewを組み合わせたこのグラフ機能のメリットとデメリット

このグラフに関してなのですが、Webページでの用途でも実装が比較的簡単なライブラリであるにも関わらずオプションが豊富でデザインやモーションが綺麗ということもあり、今回はネイティブアプリとミックスして使ってみました。しかしながらやはりWebベース(canvasで動いている)ということもあり、場合によってはHTML側での細かな調節が必要なケースもあると思います。

ですので、こちらは特にWebViewを活用するアプリの場合はグラフを作成する際の選択肢の1つとしても良いのではないかと思います。

★補足2:このグラフ機能を実際に活用した例

今回ご紹介したグラフ機能は、私がリリースしていた簡易家計簿アプリ「Coffre」でも利用しています。

promotion_coffre.jpg

こちらは、棒グラフと円グラフを表示する仕様になっていますので、実際の動きをもっと見てみたい方はインストールして確認してみて下さい。

Coffre公式ページ
Coffreのダウンロードはこちら

おわりに. 「実戦にできるだけ近しいサンプルの実装」の解説をして

本来のiOSアプリの実装と比べるとWebの技術を部分的に利用したかなりトリッキーなTipsでかつ記事の内容もかなり長いのですが最後まで読んで頂きまして本当にありがとうございます。

特に要の部分は「データの永続化でRealmを使う」部分と「入力に関する制御」部分の2つになるかと思います。

今回折角のAdventCalendarなので「できるだけ実際のアプリに近い形のサンプルを解説したい」という思いと「まだまだ経験が浅いけれども実装に当たって自分なりに気をつけた点」等を共有することによって、少しでも初学の方や実務でお悩みの方の手助けになることができればと思い、このような形を取らせて頂きました。

私自身も勉強会で、実務経験のある方からのアドバイスやレクチャー等もあり沢山の開発者や仲間に助けられました。今ここでこうして楽しくアウトプットができることに幸せと感謝の気持ちを感じております。

そしてこれからも、より良いアウトプットを自分のアプリやサービスは勿論のこと、Qiitaのような場所や勉強会でも「困っている方の背中をそっと優しく押すことができれば」と思っておりますので、今後とも宜しくお願い致します。

追記とその他

2016/10/20:

  • XCode8 & Swift3.0への対応を行いました。

ブランチはこちら:Swift3.0 & XCode8対応の私のカロリーグラフ(試作品)

(既存プロジェクトからSwift3.0へコンバートした後の注意点1)
XCode8からInfo.plistへカメラ及びフォトライブラリへアクセスするための記述を追加してあげる必要があります。こちらの対応を行う際には下記の記事を参考にInfo.plistの記載を行いました。

iOS10カスタムカメラ - Swift 3

(既存プロジェクトからSwift3.0へコンバートした後の注意点2)
Realmのバージョンを上げる必要とPodfileの記載にも変更が必要になります。Podfileの中身は下記のようになります。

自分で実装したSwiftファイルをSwift3.0へコンバートした後に、Podfileを書き直した後に一旦pod 'RealmSwift'部分をコメントアウトしてインストールして$ pod installを実行して、綺麗にした後に、コメントアウトを戻して再びインストールしてあげればOKです。
(使用しているCocoaPodsのバージョンは1.1.0.rc.2になります)

platform :ios, '9.0'
swift_version = '3.0'
use_frameworks!
target 'WebViewGraphOfSwift' do
  pod 'RealmSwift'

  post_install do |installer|
    installer.pods_project.targets.each do |target|
      target.build_configurations.each do |config|
        config.build_settings['SWIFT_VERSION'] = '3.0'
      end
    end
  end
end

プロジェクトを開き直した際に以前のキャッシュが残っていて警告が大量に出てしまう場合には、下記のコマンドを実行して一旦キャッシュをクリアしてあげるようにして下さい。

$ sudo rm -dfR /Users/(自分のユーザー名)/Library/Developer/Xcode/DerivedData/

※Swift3.0への対応時の修正箇所や注意点に関する詳しい解説はまた改めて別の記事にて行う予定です。

2016/01/12:

  • Githubのサンプルの写真追加部分に[UIImagePickerControllerのインスタンス].allowsEditing = trueで編集可能にする対応をしました。

2015/12/22:

  • GithubへのPull Requestならびに要望や改善に関する提案も受け付けていますのでお気軽にどうぞ!
  • 1. データの永続化にはRealmを使用
  • 2. 写真に関する処理の総まとめ
  • 3. テキストフィールドやデイトピッカーの制御
  • 4. テーブルビューの基本的なカスタマイズ
  • 5. ローカルのHTML / JavaScriptを利用したグラフ描画
  • おわりに. 「実戦にできるだけ近しいサンプルの実装」の解説をして
  • 追記とその他