はじめに
みなさん、はじめまして。 走れるシステムエンジニアこと、溝口です。 実業団の陸上部を引退して、去年の1月にテラスカイに入社してから今まで、本当に色々な方々に支えられて、エンジニアとしての経験を積むことが出来ました。 そんな中でも、今回は縁があってモバイルアプリケーションの開発に携わる事ができ、これは是非自分からもみなさんに何か伝えたい!という思いでこの記事を書き始めました。
今回はSalesforceとモバイル連携の1つのキーワードでもある「Salesforce Mobile SDK」を使用したアプリケーション、その中で恐らく需要が高いであろうオフライン対応の部分について触れてみたいと思います。
Salesforce Mobile SDKとは
みなさんご存知の通り、Salesforceから提供されているモバイル開発向けのSDKです。 開発出来るアプリケーションとして、Native(iOS、Android)、Hybrid(HTML5+Javascript)、HTML5が挙げられます。
このSDKを使って何が出来るか、というのは公式マニュアルから
・Salesforce REST API コールを簡単にするクラスとインターフェース
・完全に実装された OAuth ログインとパスコードのプロトコル
・ユーザデータをオフラインで安全に管理するためのSmartStore ライブラリ
の主に3つの機能が提供されていることが分かります。 その中で、今回はNative(iOS)アプリケーションのSmartStoreを使ってみたいと思います。
SmartStoreについて
『ユーザデータをオフラインで安全に管理するためのSmartStore ライブラリ』という説明からも分かる通り、Salesforceから取得したレコードをモバイル機器の内部ストレージに安全に保存する為の仕組みになります。 使用する際にはスープと呼ばれる一時領域を定義して使います。 内部的にFMDatabaseクラス(iOSの内部DBであるSQLiteを扱うためのクラス)をラッピングしているので、Salesforce仕様の内部DBを簡単に扱えるようになるクラス、という認識で問題無いと思います。
スープ、及びスープのデータの保持期間はリフレッシュトークンの有効期間となります。 (トークン、及びOAuth認証についてはコチラを参照して下さい) ログアウト処理を行うか、リフレッシュトークンの有効期間が切れると自動でSmartStoreのデータとスープ領域は消失します。
SmartStoreのスープの定義方法について
今回はSalesforce Mobile SDKのサンプルアプリケーションのNativeSqlAggregatorから、 SmartStoreの定義部分をAccountオブジェクト仕様に簡易的に変更したものを使用します。 (Salesforce Mobile SDKのサンプルアプリケーションについてはコチラを参照して下さい)
では、コード上ではどのようにしてスープを定義していくのか、見てみましょう。
スープの定義
NSString* const kAccountSoupName = @"Account"; | |
NSString* const kAllAccountQuery = @"SELECT {Account:Id},{Account:Name} FROM {Account}"; | |
@interface SmartStoreInterface () | |
@property (nonatomic, strong) SFSmartStore *store; | |
@end | |
@implementation SmartStoreInterface : NSObject | |
- (id)init | |
{ | |
self = [super init]; | |
if (nil != self) { | |
//kDefaultSmartStoreNameはデフォルトで@"defaultStore"が設定されています | |
_store = [SFSmartStore sharedStoreWithName:kDefaultSmartStoreName]; | |
} | |
return self; | |
} | |
- (void)createAccountSoup | |
{ | |
if (![_store soupExists:kAccountSoupName]) { | |
//ここで各項目に対応するキーの値を設定します(これは固定です) | |
NSArray *keys = @[@"path", @"type"]; | |
//「項目名, 型」でスープ領域で仕様する項目を定義します | |
NSArray *nameValues = @[@"Name", kSoupIndexTypeString]; | |
NSDictionary *nameDictionary = [NSDictionary dictionaryWithObjects:nameValues forKeys:keys]; | |
NSArray *idValues = @[@"Id", kSoupIndexTypeString]; | |
NSDictionary *idDictionary = [NSDictionary dictionaryWithObjects:idValues forKeys:keys]; | |
//ここで各項目のIndexを作成します | |
NSArray *practiceIndexSpecs = @[idDictionary, nameDictionary]; | |
[_store registerSoup:kAccountSoupName withIndexSpecs:practiceIndexSpecs]; | |
} | |
} |
ここでは主にSmartStoreのスープ領域で使用する項目の定義を行います。 SQLでいうCREATE文と同じような処理だと思って頂いて構いません。
7行目で各項目に対応するキーの値を設定します(これは固定です) 10?14行目で項目を定義しています。「項目名, 型」で定義します 型は
・kSoupIndexTypeString
・kSoupIndexTypeInteger
・kSoupIndexTypeFloating
の3種類があります (今回はId、Name共にString型なので、kSoupIndexTypeStringで定義しています)
17行目で各項目のIndexを作成します。 ※ここで設定した並び順がデータを取得する際の配列のIndex番号として設定されます。 例えば、今回定義したスープから取得した値を「record」というNSArray形式の変数に格納したとして、record[0]とするとIdのデータが、record[1]とするとNameのデータを取得することが出来ます
スープへのデータ挿入方法
- (void)upsertAccount:(NSDictionary*)account | |
{ | |
if (nil != account) { | |
[_store upsertEntries:[NSArray arrayWithObject:account] | |
toSoup:kAccountSoupName]; | |
} | |
} |
「項目名:データ」の形式でNSDictionary型の値を作成し、NSArrayの配列に入れてupsertEntriesメソッドを呼び出します。
スープからのデータ取得方法
NSString* const kAllAccountQuery = @"SELECT {Account:Id},{Account:Name} FROM {Account}"; | |
- (NSArray*)getAccount | |
{ | |
SFQuerySpec * querySpec = [SFQuerySpec newSmartQuerySpec:kAllAccountQuery withPageSize:10]; | |
return [_store queryWithQuerySpec:querySpec pageIndex:0]; | |
} |
Smart SQLというSmartStore独自のクエリを使用してデータを取得します。 今回はAccountスープの全件を取得するクエリを使用します。 あまり見慣れない形式ではありますが、『SELECT {スープ名:項目名} FROM {スープ名}』の形式でクエリを発行します。 Smart SQLについて詳しく知りたい場合はコチラを参照して下さい。
SmartStoreを使ってみよう!
では、定義したSmartStoreのスープ領域を使用した簡単な実装例を紹介します。 下記画面が今回使用するアプリケーションの画面です。 今回はSmartStoreの実装方法の方をメインに紹介しますので、画面の作成手順に関しては割愛させて頂きます。
【メイン画面】(RootViewController)
Accountオブジェクトのレコードを最大10件取得し、表示するビューです。
【編集画面】(DetailViewController)
メイン画面で対象のレコードをタップした際に遷移する編集画面です。 ここで編集できるのはNameのみとしています。
【一時保存画面】(TempViewController)
SmartStoreに保存したレコードの一覧を表示する画面です。 今回は表示機能のみ実装しています。
実装例について
今回は以下の様な処理を想定しています。
① Salesforceにクエリを投げ、データを取得
② 対象のレコードをタップし、編集画面に遷移
③ 編集画面でデータを編集し、保存時にネットワークエラー
④ 保存しようとしたデータをSmartStoreのスープ領域に保存
⑤ 一時保存画面でスープ領域にデータが保存されていることを確認
スープ領域を作成
- (void)viewDidLoad | |
{ | |
[super viewDidLoad]; | |
self.title = @"SmartStore Sample App"; | |
//スープ領域を作成 | |
[[[SmartStoreInterface alloc] init] createAccountSoup]; | |
} |
メイン画面の読み込み時にスープ領域を作成しています。 既に同名のスープ領域が存在する場合は処理はスキップされます。
Salesforceにクエリを投げ、データを取得
@property (nonatomic, strong) NSArray *dataRows; | |
- (void)viewWillAppear:(BOOL)animated | |
{ | |
[super viewWillAppear:animated]; | |
SFRestRequest *request = [[SFRestAPI sharedInstance] requestForQuery:@"SELECT Id, Name FROM Account LIMIT 10"]; | |
[[SFRestAPI sharedInstance] send:request delegate:self]; | |
} | |
#pragma mark - SFRestAPIDelegate | |
- (void)request:(SFRestRequest *)request didLoadResponse:(id)jsonResponse | |
{ | |
NSArray *records = [jsonResponse objectForKey:@"records"]; | |
_dataRows = records; | |
dispatch_async(dispatch_get_main_queue(), ^{ | |
[self.tableView reloadData]; | |
}); | |
} | |
- (void)request:(SFRestRequest*)request didFailLoadWithError:(NSError*)error | |
{ | |
NSLog(@"request:didFailLoadWithError: %@", error); | |
} |
メイン画面表示用のSalesforceのデータを取得します。
対象のレコードをタップし、編集画面に遷移
- (void)tableView:(UITableView *)itemTableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath | |
{ | |
[itemTableView deselectRowAtIndexPath:indexPath animated:NO]; | |
NSDictionary *obj = [_dataRows objectAtIndex:indexPath.row]; | |
DetailViewController *detailController = | |
[[DetailViewController alloc] initWithName:[obj objectForKey:@"Name"] | |
id:[obj objectForKey:@"Id"]]; | |
[self.navigationController pushViewController:detailController animated:YES]; | |
} |
タップされたレコードのId、及びNameのデータを遷移先の画面に引き渡して、 次画面に遷移します。
編集画面でネットワークエラーが発生し、Salesforceに保存しようとしていたデータをSmartStoreに保存
- (void)pressSaveButton | |
{ | |
NSDictionary *fields = @{@"Name": _nameField.text}; | |
SFRestRequest *request = [[SFRestAPI sharedInstance] requestForUpdateWithObjectType:@"Account" | |
objectId:_idData | |
fields:fields]; | |
[[SFRestAPI sharedInstance] send:request delegate:self]; | |
} | |
#pragma mark - SFRestAPIDelegate | |
- (void)request:(SFRestRequest *)request didLoadResponse:(id)jsonResponse | |
{ | |
dispatch_async(dispatch_get_main_queue(), ^{ | |
[self.navigationController popViewControllerAnimated:YES]; | |
}); | |
} | |
- (void)request:(SFRestRequest*)request didFailLoadWithError:(NSError*)error | |
{ | |
//通信エラーが発生した場合の処理 | |
if([error code] == -1009){ | |
[self tempDataUpdate]; | |
} | |
NSLog(@"request:didFailLoadWithError: %@", error); | |
} | |
- (void)tempDataUpdate | |
{ | |
NSDictionary *updateobj = @{@"Name": _nameField.text, @"Id": _idData}; | |
[[[SmartStoreInterface alloc] init] upsertAccount:updateobj]; | |
} |
編集画面でデータの保存時に圏外等で通信が出来なくなってしまった場合の処理です。
Saveの処理でSalesforceにデータを投げる際にエラーとなる為、didFailLoadWithErrorメソッドが呼び出されます。 その中でエラーコードが-1009(通信エラー)だった場合のみ、SmartStoreに対象のデータを保存しています。
この処理を入れることにより、ネットワークが途切れた場合でもSalesforceに保存予定だったデータを救済することが出来ます。
一時保存画面でスープ領域にデータが保存されていることを確認
@property (nonatomic, strong) NSArray *dataRows; | |
- (void)getTempData | |
{ | |
NSArray *results = [[[SmartStoreInterface alloc] init] getAccount]; | |
if(results){ | |
_dataRows = results; | |
[self.tableView reloadData]; | |
} | |
} |
下書きビューではSalesforceではなく、スープ領域に対してクエリを発行している為、 オフラインでもデータを編集することが可能です。(今回はスープ領域のレコードの参照、編集機能は組み込んでいません)
まとめ
みなさん、どうでしたか? モバイルアプリケーションのクラウド連携のオフライン対応、と聞くとどうしてもハードルが高い様な気がしますが、こんなにも簡単にオフライン機能を実装することが出来ました。
Salesforce Mobile SDKはSmartStoreはもちろん、その他にも非常に使いやすく、強力なモバイルアプリケーション対応機能があるので、みなさんも是非Salesforce Mobile SDKでSalesforce連携モバイルアプリケーション開発を始めてみては如何でしょうか?
※今回作成したアプリケーションに関しては私個人のGitHubアカウントにて公開しています。