FacebookAPI (iOS SDK)を拡張する

iPhone Dev THK(東北・仙台)勉強会でセッションを行いました。Facebook APIをネイティブアプリとして動かすには幾つかのハードルがあり、実際にアプリに組み込んで使うにはiOS SDKを拡張しないと現実的でないので、その拡張をライブコーディングで行いました。



Facebook APIをラップしたiOSモジュールが公式に配布されているのでそれをgithubから取得します。取得したモジュールの中にDemoAppというプロジェクトがあるので、これを使って色々と動作の確認が行えます。

実際にDemoAppを動かすには、Facebookでアプリを登録してAPI Keyを取得する必要がありますが、アプリを登録するには、まずFacebook Developersで自分のFacebookアカウントを開発者として登録が必要になります。
(※登録には携帯電話かクレジットカードが必要)


アプリの登録は「アプリ」タブの「Create New App」から登録します。アプリ登録が完了したら、概要にAPI Keyが表示されますので、このキーを DemoAppViewController.m の kAppId に設定します。
※token secretは使用しなくてもアクセス可能



DemoAppViewController.m

// Your Facebook APP Id must be set before running this example
// See http://www.facebook.com/developers/createapp.php
// Also, your application must bind to the fb[app_id]:// URL
// scheme (substitute [app_id] for your real Facebook app id).
static NSString* kAppId = @"23xxxxxxxxxxxxx";


初期設定では認証が標準のSafariで行うようになっており、認証後のコールバックにURL Schemeを使います。


DemoApp-Info.plist



※URL Schemes > Item 0 にはfb+API Key(数字) となります。



これで一通り動かせるようになるのですが、毎回Safariで認証してコールバックさせるのは現実的ではないのでFacebookが用意しているダイアログモードで認証させるようにします。
実際に認証の処理を実行するメソッドは Facebook#authorizeWithFBAppAuth:safariAuth: で行われ、この第2引数がSafariモードにするかダイアログモードにするかを決定しています。デフォルトがYESとなっているのでこれをNOに変更します。


Facebook.m

- (void)authorize:(NSArray *)permissions
       localAppId:(NSString *)localAppId {
    self.localAppId = localAppId;
    self.permissions = permissions;
    
    [self authorizeWithFBAppAuth:YES safariAuth:NO];
}


これでSafariに飛ばずにダイアログのWebViewで認証が行えるようになります。
Facebook.mはSDKコアなので、直接変更するのに違和感があるようであればカテゴリオーバーライド等で塗りつぶすのがいいかもしれません。


もう一つの難点として、Facebookaccess_tokenは有効期限が短く切れてしまうためにアプリ上では毎回認証をさせる必要があります。DemoAppでは毎回Loginボタンを押す必要があるのでこれを起動時に自動で認証させるようにします。


先ほどの Facebook#authorizeWithFBAppAuth:safariAuth:が認証をさせるためのリクエストを発行していますが、このログイン用リクエストは認証済みであればWeb画面を返さずに自動的にリダイレクトでaccess_tokenを投げ返すようになっています。これを利用してダイアログを出さずにリクエストだけ発行する仕組みを作れば自動認証が可能となります。

具体的には FBLoginDialog#show を行わずに FBLoginDialog#load だけを実行すればダイアログなしにリクエストの発行が行えます。

ここではダイアログなしで認証するカテゴリメソッドを追加して対応します。

Facebook+Add.h

#import <Foundation/Foundation.h>
#import "Facebook.h"

@interface Facebook (Add)
- (void)authorizeWithoutDialog;
@end


Facebook+Add.m

#import "Facebook+Add.h"

@implementation Facebook (Add)
static NSString* kDialogBaseURL = @"https://m.facebook.com/dialog/";
static NSString* kRedirectURL = @"fbconnect://success";
static NSString* kLogin = @"oauth";
static NSString* kSDKVersion = @"2";

/**
 * A public function for authorization without dialog.
 */
- (void)authorizeWithoutDialog {
    NSMutableDictionary* params = [NSMutableDictionary dictionaryWithObjectsAndKeys:
                                   _appId, @"client_id",
                                   @"user_agent", @"type",
                                   kRedirectURL, @"redirect_uri",
                                   @"touch", @"display",
                                   kSDKVersion, @"sdk",
                                   nil];
    NSString *loginDialogURL = [kDialogBaseURL stringByAppendingString:kLogin];
    if (_permissions != nil) {
        NSString* scope = [_permissions componentsJoinedByString:@","];
        [params setValue:scope forKey:@"scope"];
    }
    if (_localAppId) {
        [params setValue:_localAppId forKey:@"local_client_id"];
    }
    
    // If single sign-on failed, open an inline login dialog. This will require the user to
    // enter his or her credentials.
    [_loginDialog release];
    _loginDialog = [[FBLoginDialog alloc] initWithURL:loginDialogURL
                                          loginParams:params
                                             delegate:self];
    [_loginDialog load];
}
@end


後は、起動直後、ここでは DemoAppViewController#viewDidLoad で 追加したメソッドをコールするようにしました。


DemoAppViewController.m

/**
 * Set initial view
 */
- (void)viewDidLoad {
    [self.label setText:@"Please log in"];
    _getUserInfoButton.hidden = YES;
    _getPublicInfoButton.hidden = YES;
    _publishButton.hidden = YES;
    _uploadPhotoButton.hidden = YES;
    _fbButton.isLoggedIn = NO;
    [_fbButton updateImage];
    
    [_facebook authorizeWithoutDialog];
}


これで一度認証を行えば、起動時に自動的にログインを行うようになります。(※Facebook認証サーバの負荷状況によっては若干時間がかかる場合があるようです)


他にも不便な点として、コールバック関数であるFBRequestDelegateの request:didLoad: の引数が直接resultのみでFacebookからのレスポンス(jsonデータ)には、リクエスト時の情報がほとんど入っておらず、非同期でガシガシとアクセスした場合に、どのリクエストに対したレスポンスかわからない状態となってしまいます。
これを解消するためには、FBRequest#handleResponseData をオーバーライドする必要がありますが大掛かりな変更となるので、また別のエントリで行ないたいと思います。


今回、勉強会で使用したソースコードgithubにアップしました。