以前「スマホ&Heroku連携!事例に学ぶ無駄のないチーム開発の極意」と題してiPhoneアプリとRailsアプリの連携について書かせて頂いたのですが、どうにも具体的な話ができなかったのが心残りでした。なので、具体的なサンプルを元に、もう少し具体的な話をしてみたいと思います。
ソースコードの置き場所
- Rails側のサンプル https://github.com/mahm/iphone_note
- iOS側のサンプル https://github.com/mahm/Note
動作例
動作している様子を動画に録って見ました。
Rails側の見所
Rails側の見所は認証部分だと思います。
class ApplicationController < ActionController::Base protect_from_forgery unless: proc { @basic_auth_token_request } before_filter :authenticate_user! protected def authenticate_with_basic return unless params['token'] and params['token'] == "token" request_http_basic_authentication unless authenticate_with_http_basic do |email, pass| begin if sign_in(User.authenticate(email, pass)) logger.info "* Authenticated by basic auth: #{email}" @basic_auth_token_request = true return end rescue ActiveRecord::RecordNotFound => exception logger.info "[Authenticated by basic auth error] email: #{email} password: #{!!pass} (#{exception.class}: #{exception.message})" respond_to do |format| format.html { redirect_to root_path, notice: exception.message } format.json { render json: { errors: exception.message }.to_json, status: :unauthorized } end end end end end
パラメータにtoken=token
が設定されている場合にBASIC認証を行う、と言う感じのコードになっています。BASIC認証で通信する場合はCSRFトークンの要求を回避するためにunless
条件をつけています。このコードだけだと実際にアクセスされたときにBASIC認証の要求が有効にならないので、以下のようなコードを各コントローラにつけてあげて下さい。
prepend_before_filter :authenticate_with_basic
ApplicationControllerのbefore_filter
につけても、もちろん構いません。
設計についてですが、BASIC認証を行う必要があるのはiOSからのアクセスだけなので、UserAgentを見て判定しても良いかも知れません。とはいえこのあたりをあまり絞り込みすぎてもテストがしづらくなる一方なので、何かしらのパラメータがある場合にBASIC認証モードにする、というのがお手軽かなーと思います。
そもそもBASIC認証なんぞ使わない、という論点もあります。その場合はxAuthを使うのがお手軽でしょう。サーバ側にはOAuth Providerとしてのソースコードが増えることになりますが、Railsなら簡単に書けますね。
後はCookieを保持してセッションを張る方法もありますが、AFNetworking
で書く方法が分かりません……ご存知の方がいたら、教えていただけたらありがたいなーと思います。
iOS側の見所
iOS側ではAFNetworkingというライブラリを使って通信を組み立てます。AFHTTPClient
クラスを継承したクラスを作って、お好みでサーバとのインターフェースを作っていくのが大体の方針です。
- (void)setEmail:(NSString *)email password:(NSString *)password; - (void)getIndexWhenSuccess:(void (^)(AFHTTPRequestOperation *operation, id responseObject))success - (void)putNoteWithId:(int)noteId - (void)createNoteWithTitle:(NSString *)title - (void)destroyNoteWithId:(int)noteId
#import "AFHTTPClient.h" @interface MLNoteClient : AFHTTPClient + (MLNoteClient *) sharedClient; // Noteの一覧を取得する failure:(void (^)(int statusCode, NSString *errorString))failure; // 指定したnoteIdのNoteを更新する title:(NSString *)title body:(NSString *)body success:(void (^)(AFHTTPRequestOperation *operation, id responseObject))success failure:(void (^)(int statusCode, NSString *errorString))failure; // Noteを新規作成する body:(NSString *)body success:(void (^)(AFHTTPRequestOperation *operation, id responseObject))success failure:(void (^)(int statusCode, NSString *errorString))failure; // 指定したnoteIdのNoteを削除する success:(void (^)(AFHTTPRequestOperation *operation, id responseObject))success failure:(void (^)(int statusCode, NSString *errorString))failure; @end
今回はNoteモデルに対するCRUDを作ります。サーバと通信するクライアントはシングルトンにしておくのが良いでしょう。
- (id) init - (void)setEmail:(NSString *)email password:(NSString *)password - (void)getIndexWhenSuccess:(void (^)(AFHTTPRequestOperation *operation, id responseObject))success - (void)putNoteWithId:(int)noteId - (void)createNoteWithTitle:(NSString *)title - (void)destroyNoteWithId:(int)noteId - (int) statusCodeFromOperation:(AFHTTPRequestOperation *)operation - (NSString *)errorStringFromOperation:(AFHTTPRequestOperation *)operation
#import "MLNoteClient.h" #define kBaseUrl @"http://iphone-note.dev/" #define kToken @"token" @interface MLNoteClient () @end @implementation MLNoteClient #pragma mark - Class methods static MLNoteClient* _sharedClient; + (MLNoteClient *) sharedClient { if (!_sharedClient) { _sharedClient = [[MLNoteClient alloc] init]; } return _sharedClient; } #pragma mark - Instance methods { if (self = [super initWithBaseURL:[NSURL URLWithString:kBaseUrl]]) { // initialize code } return self; } { [self clearAuthorizationHeader]; [self setAuthorizationHeaderWithUsername:email password:password]; } failure:(void (^)(int statusCode, NSString *errorString))failure { NSDictionary *params = [NSDictionary dictionaryWithObjectsAndKeys: kToken, @"token", nil]; [self getPath:@"notes.json" parameters:params success:success failure:^(AFHTTPRequestOperation *operation, NSError *error){ failure([self statusCodeFromOperation:operation], [self errorStringFromOperation:operation]); }]; } title:(NSString *)title body:(NSString *)body success:(void (^)(AFHTTPRequestOperation *operation, id responseObject))success failure:(void (^)(int statusCode, NSString *errorString))failure { NSDictionary *note = [NSDictionary dictionaryWithObjectsAndKeys: title, @"title", body, @"body", nil]; NSDictionary *params = [NSDictionary dictionaryWithObjectsAndKeys: kToken, @"token", note, @"note", nil]; [self putPath:[NSString stringWithFormat:@"notes/%d.json", noteId] parameters:params success:success failure:^(AFHTTPRequestOperation *operation, NSError *error){ failure([self statusCodeFromOperation:operation], [self errorStringFromOperation:operation]); }]; } body:(NSString *)body success:(void (^)(AFHTTPRequestOperation *operation, id responseObject))success failure:(void (^)(int statusCode, NSString *errorString))failure { NSDictionary *note = [NSDictionary dictionaryWithObjectsAndKeys: title, @"title", body, @"body", nil]; NSDictionary *params = [NSDictionary dictionaryWithObjectsAndKeys: kToken, @"token", note, @"note", nil]; [self postPath:@"notes.json" parameters:params success:success failure:^(AFHTTPRequestOperation *operation, NSError *error){ failure([self statusCodeFromOperation:operation], [self errorStringFromOperation:operation]); }]; } success:(void (^)(AFHTTPRequestOperation *operation, id responseObject))success failure:(void (^)(int statusCode, NSString *errorString))failure { NSDictionary *params = [NSDictionary dictionaryWithObjectsAndKeys: kToken, @"token", nil]; [self deletePath:[NSString stringWithFormat:@"notes/%d.json", noteId] parameters:params success:success failure:^(AFHTTPRequestOperation *operation, NSError *error){ failure([self statusCodeFromOperation:operation], [self errorStringFromOperation:operation]); }]; } #pragma mark - Helper methods { return operation.response.statusCode; } { return [[operation.responseData objectFromJSONData] valueForKey:@"errors"]; } @end
Railsの例と違って、やたらコードが長いのが玉にキズですね。
BASIC認証
BASIC認証は以下のコードで実現しています。
- (void)setEmail:(NSString *)email password:(NSString *)password
{ [self clearAuthorizationHeader]; [self setAuthorizationHeaderWithUsername:email password:password]; }
emailとpasswordを設定した時点で、Authorization Headerに値を設定しています。これで通信を行うときはいつでも認証を通すことになります。
エラーコードが返ってきた時のブロック
AFHTTPClient
のメソッドのままだと、失敗時の引数がAFHTTPRequestOperation
で何とも使いにくいので、上のコードではステータスコードとサーバ側のエラーメッセージを返すようにしています。
- (void)getIndexWhenSuccess:(void (^)(AFHTTPRequestOperation *operation, id responseObject))success
failure:(void (^)(int statusCode, NSString *errorString))failure { NSDictionary *params = [NSDictionary dictionaryWithObjectsAndKeys: kToken, @"token", nil]; [self getPath:@"notes.json" parameters:params success:success failure:^(AFHTTPRequestOperation *operation, NSError *error){ failure([self statusCodeFromOperation:operation], [self errorStringFromOperation:operation]); }]; }
実際に使っているところ
- (void)viewDidAppear:(BOOL)animated
{ [super viewDidAppear:animated]; //---------------------------------------------------- // 表示時にサーバ上のデータを読み込む [SVProgressHUD showWithStatus:@"Loading" maskType:SVProgressHUDMaskTypeBlack]; [[MLNoteClient sharedClient] getIndexWhenSuccess:^(AFHTTPRequestOperation *operation, id responseObject){ _notes = [[NSMutableArray alloc] initWithArray:[responseObject objectFromJSONData]]; [self.tableView reloadData]; [SVProgressHUD dismiss]; } failure:^(int statusCode, NSString *errorString){ // 認証されていない場合はアカウントの設定画面に飛ばす if (statusCode == 401) { [self.tabBarController setSelectedIndex:1]; } UIAlertView *alert = [[UIAlertView alloc] initWithTitle:errorString message:nil delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil, nil]; [alert show]; [SVProgressHUD dismiss]; }]; }
ブロックで書けるのが良いですねー。
まとめ
認証さえ何とかなれば、比較的簡単にサーバと連携できることが分かりました。あとはクライアントでデータをキャッシュしたりして、無駄な通信を減らしたりする事で、よりUXを高めることができると思います。
この記事が皆様のご参考になれば幸いです。