ニフティクラウドmobile backendを利用した画像付きメモアプリ作成のポイントまとめ(Parse.comとの比較付き)


はじめに:ニフティクラウドmobile backendを使ってまずはサンプルアプリを作成してみます。

前回はニフティクラウドmobile backendをCocoaPods経由でインストールする手順と実際にテストデータ1件を登録または呼び出しをする疎通試験を行うところまでを行いました。実際の手順や概要等に関しては下記をご参照頂ければと思います。

今回は以前に作成したニフティクラウドmobile backendのSDKと疎通試験をしたコードに実際の画面と処理を追記した上で実際のサンプルアプリの基本形になる部分を作成していこうと思います。またParse.comを使用した際の処理との書き方の違いや似ている点なども出来るだけ見比べをして作成したのでその際のことも合わせて解説をしていきます。

(かなり基本的なサンプルにはなりますがその点は宜しくお願い致します)


準備:写真付き買い物メモアプリサンプルを作成していきます。

今回のサンプルを作成した私の方の使用環境は下記になります:

・OS X 10.10.5 Yosemite

・XCode 7.2

・Swift2.1.1

※そろそろEl Capitanに上げなければ...

今回はParse.comで作成したサンプルよりもシンプルにデータの一覧表示と追加・変更・削除ができるアプリになります。またデータの追加・変更をする際にはカメラ撮影した画像ないしはフォトライブラリー内の画像も一緒に登録ができるようにします。

そして各々の処理でポイントとなる部分にはParse.comでの処理の記載と似ている部分と異なる部分についても触れていきます。

(参考1)サンプルアプリの設計図:

niftycloud_app.png

今回は下記の2画面だけで構成されているシンプルな構成にしてあります。


  • ViewController.swift:登録されているデータをUITableViewで一覧表示をする画面

  • AddController.swift:データの追加・変更・削除を行うための画面

データを扱うアプリの一番基本形となる部分をしっかりと押さえておくと他に似たような処理を作成する場合でも応用が利くと思います。

(参考2)Storyboardの画面遷移図:

storyboard_sample.png

2つのViewControllerをSegueでつなげているだけのベタな感じにしています。ViewController.swiftに対応する画面の一番下にある「新しくデータを追加する」を押すと追加モードとして、表示されているデータのセルをタップすると更新モードとしてAddController.swiftに対応する画面に遷移します。

(Xcodeで新規プロジェクトを作成する際に「Master-Detail Application」を使用して要素を配置しても可能です)


  • ViewController.swift:登録されているデータをUITableViewで一覧表示をする画面

  • AddController.swift:データの追加・変更・削除を行うための画面

追加と更新の判定はSegueで値を渡す際に追加なのか更新なのかを判定するためのフラグ(Bool型)によって決定しています。またデータの削除(ゴミ箱のボタン)は編集モードの際にのみ活性となります。

(参考3)ニフティクラウドmobile backend側のテーブル定義:

今回はあらかじめ入れておくデータ等は特になく、単一のテーブルのみを使用する形にします。また新規追加の際にテーブルのクラスに該当するカラムが自動的に作成されますので特にニフティクラウドmobile backend側でテーブル定義の設定をする必要は特にありません。

◎ MemoClass:メモ情報マスタ

カラム名
データ型
説明

objectId
文字列
一意なID

money
数字
金額

title
文字列
メモのタイトル

filename
文字列
アップロードされたファイル名

comment
文字列
メモのコメント

createDate
日付
登録日

updateDate
日付
更新日

ACL
ACL
アクセスコントロール

※ objectId, ACL, createDate, updateDateに関しては新規追加時に自動的に設定されます。

画像データの持たせ方に関してParse.comと異なる点としては、Parse.comは該当したカラム内にFile型を指定して、直接そのカラム内にファイルデータを格納する仕様になっています。

ニフティクラウドmobile backendは管理画面を見てみると「データストア」と「ファイルストア」という項目がナビゲーションに存在しています。

datastore_capture.png

filestore_capture.png

テーブルに格納されるデータはナビゲーションのデータストアのところから見ることができます。この中にfilenameというカラムが存在するのですが、その中にはあくまでファイルストアの中に格納されているファイルの名前があるだけです(後述のコードにて詳細を解説します)。

ですので画像やその他ファイルを呼び出す場合の処理がParse.comの場合とは違ってきますのでParse.comからニフティクラウドmobile backendへ乗り換える際には注意してください。


コード:実装のポイントとParse.comでの見比べの解説とまとめを記載していきます。

それでは実際にこのサンプルで実装されているコードに関する部分を深掘りしていきたいと思います。Swift2系で基本的なデータの追加・変更・削除の処理を実装するにあたっては下記の記事を参考にした上で、本サンプル用にアレンジを加えた形になっています。

今回のサンプルでニフティクラウドmobile backendに関する処理の概要は下記のようになります。

①ViewController.swift:


  • 登録されているデータを全件を新しく登録された順に取得する

  • UITableViewに取得したデータを表示&画像データの取得処理をバックグラウンドで実行する

②AddController.swift:


  • 文字列及び数字データの追加・変更・削除

  • 画像データの追加・削除処理をバックグラウンドで実行する

上記の部分に関してそれではコードの解説と見比べを行っていきたいと思います。

(Parse.comのと見比べをする部分に関してはポイントとなる部分について)


★(ViewController.swft)登録されているデータを全件を新しく登録された順に取得する処理をニフティクラウドmobile backendで実現する


ViewController.swift

--- (class内の記述) ---

//Memoデータを格納する場所
var memoArray: NSArray = NSArray()

--- (中略) ---

//データのリロード
func loadMemoData() {

/**
* Just FYI
*
* Example: 文字列と一致する場合
* query.whereKey("title", equalTo: "xxx")
*
* メソッドのインターフェイスについて:
* NCMBQuery.hを参照するとNCMBQueryのインスタンスメソッドの引数にとるべき値等が見れます。
*
*/

let query: NCMBQuery = NCMBQuery(className: "MemoClass")
query.orderByDescending("createDate")
query.findObjectsInBackgroundWithBlock({(NSArray objects, NSError error) in

if error == nil {
if objects.count > 0 {
self.memoArray = objects

//テーブルビューをリロードする
self.memoTableView.reloadData()
}
} else {
print(error.localizedDescription)
}
})
}


上記のコードでは、ニフティクラウドmobile backendよりデータの一覧を取得してNSArray型のメンバ変数内に格納しておく処理になります。通信が正常に実行された際にはNSArray型の取得したオブジェクト(変数名:objects)が返却されるので、クロージャー内にメンバ変数に格納する処理を記載すればOKです。

下記は参考までにfindObjectsInBackgroundWithBlockメソッドのインターフェースになります。


NCMBQuery.h

/**

設定されている検索条件に当てはまるオブジェクトを非同期で取得。取得し終わったら与えられたblockを呼び出す。
@param block 信後に実行されるblock。blockは次の引数のシグネチャを持つ必要がある(NSArray *objects, NSError *error)
objectsには取得したオブジェクトが渡される。errorにはエラーがなければnilが渡される。
*/

- (void)findObjectsInBackgroundWithBlock:(NCMBArrayResultBlock)block;

Parse.comでも同様にfindObjectsInBackgroundWithBlockメソッドがありますが、それぞれを比べてみると正常処理時の取得したオブジェクトの型が異なっていますので、乗り換えにあたって処理を書き換える必要がある場合には参考にしてみて下さい。


ViewController.swift(もしParse.comを使用した場合の例)

--- (class内の記述) ---

//Memoデータを格納する場所
var memoArray: NSMutableArray = NSMutableArray()

--- (中略) ---

//データのリロード
func loadMemoData() {

//いったん空っぽにしておく
self.memoArray.removeAllObjects()

let query: PFQuery = PFQuery(className: "MemoClass")
//登録日・更新日はParse.comの場合はcreatedAt, updatedAtとなります
query.orderByDescending("createdAt")
query.findObjectsInBackgroundWithBlock {
(objects:[PFObject]?, error:NSError?) -> Void in

if error == nil {
//データが存在する場合はNSMutableArrayへデータを格納
if let objects = objects {
for object in objects {
//取得したオブジェクトをメンバ変数へ格納
self.memoArray.addObject(object)
}
//テーブルビューをリロードする
self.memoTableView.reloadData()
}
} else {
//異常処理の際にはエラー内容の表示
print("Error: \(error!) \(error!.userInfo)")
}
})
}


上記についてはほんの一例ではありますが、意外と同じ名前のメソッドがあったりするので、あとは返ってくるデータの型に注意すると良いかと思います。


★(ViewController.swft)UITableViewに取得したデータを表示&画像データの取得処理をバックグラウンドで実行する

※ニフティクラウドmobile backend及びParse.comのデータは前述のロジックで取得していることを前提とします。


ViewController.swift

--- (class内の記述) ---

//※self.memoArray内にはNSArray型のNCMBからのデータが入っている

//表示するセルの中身を設定する ※必須
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {

let cell = tableView.dequeueReusableCellWithIdentifier("MemoCell") as? MemoCell

//各値をセルに入れる
let targetMemoData: AnyObject = self.memoArray[indexPath.row]
print(targetMemoData)

cell!.memoTitle.text = targetMemoData.objectForKey("title") as? String
cell!.memoMoney.text = String(targetMemoData.objectForKey("money")!)
cell!.memoComment.text = targetMemoData.objectForKey("comment") as? String

cell!.memoImage.image = nil

//画像データの取得
let filename: String = (targetMemoData.objectForKey("filename") as? String)!
let fileData = NCMBFile.fileWithName(filename, data: nil) as! NCMBFile

fileData.getDataInBackgroundWithBlock { (imageData: NSData?, error: NSError!) -> Void in

if error != nil {
print("写真の取得失敗: \(error)")
} else {
cell!.memoImage.image = UIImage(data: imageData!)
}
}

cell!.selectionStyle = UITableViewCellSelectionStyle.None
cell!.accessoryType = UITableViewCellAccessoryType.None

return cell!
}


UITableViewCell内のプロパティに値をセットする処理に関しては、取得したデータを格納したメンバ変数をself.memoArray[indexPath.row]の形で分解してそれぞれ表示させます。この中でも特にParse.comと大きく違う点は画像の取得処理に関する部分です。

ニフティクラウドmobile backendの場合はまずfilenameを取得し、その値を元にlet fileData = NCMBFile.fileWithName(filename, data: nil) as! NCMBFileの形でNCMBFile型のデータを取得した上でgetDataInBackgroundWithBlockメソッドにて画像の取得をバックグラウンド実行で取得する形の実装になります。

Parse.comの場合は、直接当該カラム内にFile型のデータを格納することができるので下記のような実装になります(同様にgetDataInBackgroundWithBlockメソッドがあるが振る舞いが異なる)。またデータの取得や表示の際の処理も参考までに比べてみるとよいかと思います。


ViewController.swift(もしParse.comを使用した場合の例)

--- (class内の記述) ---

//※self.memoArray内にはNSMutableArray型のParse.comからのデータが入っている

//表示するセルの中身を設定する ※必須
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {

let cell = tableView.dequeueReusableCellWithIdentifier("MemoCell") as? MemoCell

//各値をセルに入れる
let targetMemoData: AnyObject = self.memoArray.objectAtIndex(indexPath.row)
print(targetMemoData)

cell!.memoTitle.text = targetMemoData.valueForKey("title") as? String
cell!.memoMoney.text = String(targetMemoData.valueForKey("money")!)
cell!.memoComment.text = targetMemoData.valueForKey("comment") as? String

cell!.memoImage.image = nil

//画像データの取得
let fileData: PFFile = (cafe.valueForKey("MemoImage") as? PFFile)!
fileData.getDataInBackgroundWithBlock {
(imageData: NSData?, error: NSError?) -> Void in
if error == nil {
let image = UIImage(data: imageData!)
cell!.commentImageView.image = image
self.commentImage = image
}
}

cell!.selectionStyle = UITableViewCellSelectionStyle.None
cell!.accessoryType = UITableViewCellAccessoryType.None

return cell!
}


大まかな部分でParse.comと比較してみるとデータ取得に関するメソッドに関してはメソッド名は同じ名前で中の振る舞いが違うような印象を受けました。今までParse.comを使っていて新しくこちらを検討される際などでもそれほど違和感なく使えるかなとは思いました。


★(AddController.swft)文字列及び数字データの追加・変更・削除

UITextFieldやカメラから取得した情報をニフティクラウドmobile backendへ登録を行う処理を記載していきます。データの追加・変更処理に関しては下記のように実装を行います。


AddController.swift

--- (データの追加変更に関する処理 @IBAction func addDataAction(sender: UIBarButtonItem)の記述を一部抜粋) ---

//保存対象の画像ファイルを作成する
let imageData: NSData = UIImagePNGRepresentation(self.targetDisplayImage!)!
let targetFile = NCMBFile.fileWithData(imageData) as! NCMBFile

//NCMBへデータを登録・編集をする
if self.editFlag == true {

//既存データを1件更新する
var saveError: NSError? = nil
let obj: NCMBObject = NCMBObject(className: "MemoClass")
obj.objectId = self.targetMemoObjectId
obj.fetchInBackgroundWithBlock({(NSError error) in

if (error == nil) {

obj.setObject(self.targetTitle, forKey: "title")
obj.setObject(targetFile.name, forKey: "filename")
obj.setObject(self.targetMoney, forKey: "money")
obj.setObject(self.targetCommnet, forKey: "comment")
obj.save(&saveError)

} else {
print("データ取得処理時にエラーが発生しました: \(error)")
}
})

//ファイルは更新があった際のみバックグラウンドで保存する
if targetFile.name != self.targetFileName {

targetFile.saveInBackgroundWithBlock({ (error: NSError!) -> Void in

if error == nil {
print("画像データ保存完了: \(targetFile.name)")
} else {
print("アップロード中にエラーが発生しました: \(error)")
}

}, progressBlock: { (percentDone: Int32) -> Void in

// 進捗状況を取得します。保存完了まで何度も呼ばれます
print("進捗状況: \(percentDone)% アップロード済み")
})
}

if saveError == nil {
print("success save data.")
} else {
print("failure save data. \(saveError)")
}

} else {

//新規データを1件登録する
var saveError: NSError? = nil
let obj: NCMBObject = NCMBObject(className: "MemoClass")
obj.setObject(self.targetTitle, forKey: "title")
obj.setObject(targetFile.name, forKey: "filename")
obj.setObject(self.targetMoney, forKey: "money")
obj.setObject(self.targetCommnet, forKey: "comment")
obj.save(&saveError)

//ファイルはバックグラウンド実行をする
targetFile.saveInBackgroundWithBlock({ (error: NSError!) -> Void in

if error == nil {
print("画像データ保存完了: \(targetFile.name)")
} else {
print("アップロード中にエラーが発生しました: \(error)")
}

}, progressBlock: { (percentDone: Int32) -> Void in

// 進捗状況を取得します。保存完了まで何度も呼ばれます
print("進捗状況: \(percentDone)% アップロード済み")
})

if saveError == nil {
print("success save data.")
} else {
print("failure save data. \(saveError)")
}
}

--- (データの削除に関する処理 @IBAction func deleteDataAction(sender: UIBarButtonItem)の記述を一部抜粋) ---

let obj: NCMBObject = NCMBObject(className: "MemoClass")
obj.objectId = self.targetMemoObjectId
obj.fetchInBackgroundWithBlock({(NSError error) in

if (error == nil) {

obj.deleteInBackgroundWithBlock({(NSError error) in

if (error == nil) {

//削除成功時に画像も一緒に削除する
let fileData = NCMBFile.fileWithName(self.targetFileName, data: nil) as! NCMBFile
fileData.deleteInBackgroundWithBlock({(NSError error) in
print("画像データ削除完了: \(self.targetFileName)")
})

}
})

} else {
print("データ取得処理時にエラーが発生しました: \(error)")
}
})


上記の処理に関してParse.comを使用した際と大きく異なるのは、「ファイル名」と「ファイル」をそれぞれ保存する処理を記載しないといけない点が大きな違いになります。しかしながら追加・更新・削除処理の実装方法の大まかな部分はやはり若干似ている感じも印象として受けます。

下記は参考までに追加・変更・削除処理で使用するメソッドは下記の4つになります。



  • saveメソッド


  • saveInBackgroundWithBlockメソッド


  • fetchInBackgroundWithBlockメソッド


  • deleteInBackgroundWithBlockメソッド

また下記に関してのメソッドのインターフェイスもNCMBObject.hに記載されていますので、調べてみると実装のイメージも湧きやすくなるかとも思います。


NCMBObject.h

/**

mobile backendにオブジェクトを保存する。エラーをセットし、エラー内容を見る事もできる。
@param error エラーを保持するポインタ
*/

- (void)save:(NSError **)error;

/**
mobile backendにオブジェクトを保存する。非同期通信を行う。
@param userBlock 通信後に実行されるblock。引数にNSError *errorを持つ。
*/

- (void)saveInBackgroundWithBlock:(NCMBErrorResultBlock)userBlock;

/**
非同期通信を利用してmobile backendからobjectIdをキーにしてデータを取得し、指定されたコールバックを呼び出す。

fetchを実行する前にセットされた値と統合されるが、サーバー上にすでにキーがあった値は上書きされる。
@param block 通信後に実行されるblock。引数にNSError *errorを持つ。
*/
- (void)fetchInBackgroundWithBlock:(NCMBErrorResultBlock)block;

/**
非同期通信を利用してオブジェクトをmobile backendとローカル上から削除し、指定されたコールバックを呼び出す。
@param userBlock 通信後に実行されるblock。引数にNSError *errorを持つ。
*/

- (void)deleteInBackgroundWithBlock:(NCMBErrorResultBlock)userBlock;


Parse.comとの見比べに関しましては以前にParse.comでのサンプルアプリケーションを作成する手順をまとめた際に書いた下記の記事の部分と比べてみると良いかと思います。

データの追加・変更・削除に関してはしっかりと深掘りと応用を今後はできるようにしていてアウトプットをしていきたいと感じています。


★(AddController.swift)画像データの追加・削除処理をバックグラウンドで実行する

画像の登録や削除のコードに関しては、ファイルストアとデータストアが分かれているためファイルの実体の保存処理は、データストアとは分けて登録ないしは削除の処理を行っています。


AddController.swift

//保存対象の画像ファイルを作成する

let imageData: NSData = UIImagePNGRepresentation(self.targetDisplayImage!)!
let targetFile = NCMBFile.fileWithData(imageData) as! NCMBFile

//ファイルはバックグラウンド実行をする ※画像の保存
targetFile.saveInBackgroundWithBlock({ (error: NSError!) -> Void in

if error == nil {
print("画像データ保存完了: \(targetFile.name)")
} else {
print("アップロード中にエラーが発生しました: \(error)")
}

}, progressBlock: { (percentDone: Int32) -> Void in

// 進捗状況を取得します。保存完了まで何度も呼ばれます
print("進捗状況: \(percentDone)% アップロード済み")
})

//ファイルはバックグラウンド実行をする ※画像の削除
targetFile.deleteInBackgroundWithBlock({(NSError error) in
print("画像データ削除完了: \(targetFile)")
})


実際の画像を登録するまでのざっくりとした流れについてはUIImageViewからデータを取り出して「UIImage → NSData → NCMBFile」の順番で処理を行っています。

下記に参考までにParse.comでUIImageViewからのデータの変換部分を参考までに掲載しておきます。(登録処理部分が違うだけで変換処理の部分の流れはほとんど同じです)


AddController.swift(もしParse.comを使用した場合の例)

//UIImage型のデータをNSData型へ変換を行う

let imageData: NSData = UIImagePNGRepresentation(self.targetDisplayImage!)!
//さらにNSData型をPFFile型に変換する
let targetFile = PFFile(name: "ファイルの名前", data: imageData)!

私自身も最初はParse.comを使用したサンプルアプリを作成して、その後ニフティクラウドmobile backendを利用したのですが、もう少し戸惑うかなと思いましたが大まかな流れとしては似ている部分もありますので、最初の導入部分ができて、Parse.comをはじめとする他のmBaaSを使用した経験がある方であればさほど迷わずに導入や移植ができるかもと思いました。


補足情報:ニフティクラウドmobile backendのメソッドのインターフェイスを確認する

ニフティクラウドmobile backendのメソッド自体はObjective-CのファイルをBridging-Header経由で呼び出している形になっています。

「Pods → NCMB」内に実際のObjective-Cのファイル群が格納されていますので、その中を参照すると使用しているメソッドのインターフェイスとメソッドの振る舞いを深掘りして見ることができます。

(参考)NCMB内のインタフェースの画面:

internal_podsfile.png

NCMBフォルダの中には多くのファイルが存在していますが、今回の部分に関しては上記のソースで幾つかピックアップしたように、NCMBQueryやNCMBObjectに関するファイルを見たりして確認しました。(私自身まだそれほど詳しというわけではありませんが)

上記の公式ドキュメント関してはまだSwift版はないのですが、今回の作成に関してはObjective-Cのチュートリアルやサンプルアプリを参考にしてSwiftに置き換えていく感じのイメージになります。


おわりに:今後追いかけていきたい部分に関して

今回は少し実践的なアプリを作成する事始めの段階の小さなサンプルアプリを作成する手順を自分なりにまとめてみた感じですが、今後はTwitter / Facebookなどのソーシャルのアカウントの連携方法やその他自分で作成したアプリ等に活用する際の例等も試した上で提示する事ができればと思っています。更新自体は結構不定期になりますが、今後の参考になることができれば幸いに思います。


追記とその他

2016.03.28

こちらに掲載しているサンプルコードをXCode7.3へ対応しましたので、修正した差分を下記に掲載をしておきました。

2016.02.02

このサンプルについて


  • GithubへのPull Requestならびに要望や改善に関する提案も受け付けていますのでお気軽にどうぞ!