【教學】Facebook SDK 首部曲:登入Facebook(這是我的一小步...卻是人類...咦?這麼多人啊?)



這篇我們會寫一個小App ,利用Facebook SDK進行登入。


App的流程:

使用者先看到LoginView,
如果已經登入Facebook並授權,帶使用者到MainView,否則使用者可以點擊FB Login按鈕進行登入,

MainView有按鈕可以讓使用者讀取自身的資料以及登出。

需要準備:
1. 已經設定好的專案,可以參考上一篇:【教學】Facebook SDK 前傳:準備Social一下
2. 先閱讀:Understanding FBSession,會有幫助。



開始吧!

先準備好專案要用的元件:


1. 建立一個LoginViewController,LoginView放上一個登入按鈕,加入點擊事件。



2. 建立一個MainViewController,MainView放上一個UILabel用來顯示‘已登入’,一個UITextView來顯示使用者資料,兩個UIButton一個抓使用者資料,一個登出。



準備好,要Coding了:


開始Copy,Coding之前我們根據看過'Understanding FBSessions'瞭解:

1. 'FBSession' class是用來管理、儲存、更新Token(憑證)的類別,所以我們不用在自己寫符合OAuth2.0流程的認證程序,讓FBSession去溝通就好,通常Session指的是FBSession Class的實體。
2. 'FBSession.activeSession'這個property是目前有效、使用的Session。
3. 透過'FBSession.activeSession.state'這個property,我們可以知道目前Session進行認證的程序狀況。

我們也瞭解'state'有下面幾種狀況

    FBSessionStateCreated                       :已建立新的Session,未找到有可用的Token。
    FBSessionStateCreatedOpening         : 已建立新的Session,使用者登入授權中。
    FBSessionStateOpen                           : 使用者已登入並授權。
    FBSessionStateCreatedTokenLoaded : 已建立新的Session,並找到可用的Token。
    FBSessionStateClosedLoginFailed      : 登入失敗、取消、不授權或是按了Home鍵退出App(state = FBSessionStateCreatedOpening)
後返回(state = FBSessionStateClosedLoginFailed)。

    FBSessionStateClosed                         : 關閉Session,但Token留著。
    FBSessionStateExtended                     : 成功要求更多的存取權限,Token會更新。


所以,我們可以在使用者進到LoginView查看是否有可用的Token(state = 'FBSessionStateCreatedTokenLoaded'),
如果有,就建立一個新的Session(後面還要溝通用)然後把使用者帶到MainView,
否則就讓使用者自己登入。



我們來Coding吧!

LoginViewController.m
#import "LoginViewController.h"
#import <FacebookSDK/FacebookSDK.h>
#import "MainViewController.h"

@interface LoginViewController ()

@end

@implementation LoginViewController

- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
{
    self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
    if (self) {
        
        self.permissions = @[@"basic_info",
                          @"user_location",
                          @"user_birthday",
                          @"user_likes"];
    }
    return self;
}

- (void)dealloc {
    [_permissions release];
    [super dealloc];
}

- (void)viewDidLoad
{
    [super viewDidLoad];
    //  cause this view is not even show on the screen, so if present a ViewController here will fail.
}

- (void)viewDidAppear:(BOOL)animated {
    [self checkState];
}

- (IBAction)loginButtonAction:(id)sender {
        NSLog(@"check session and token is vaild :");
        //  check if user already login or not
        //  little bug at LoginUI in webview, you can close it by click left-up corner.
    
    [FBSession openActiveSessionWithReadPermissions:_permissions
                                           allowLoginUI:YES
                                      completionHandler:^(FBSession *session,
                                                          FBSessionState status,
                                                          NSError *error) {
                                          //    called while state changed
                                          
                                          [self checkState];

                                      }];
}

- (void)checkState{
    
    FBSessionState nowState = FBSession.activeSession.state;
    
    switch (nowState) {
        case FBSessionStateClosed:
            NSLog(@"FBSessionStateClosed");
            break;
            
        case FBSessionStateClosedLoginFailed:
            NSLog(@"FBSessionStateClosedLoginFailed");
            break;
            
        case FBSessionStateCreated:
            NSLog(@"FBSessionStateCreated");
            NSLog(@"No Login");
            break;
            
        case FBSessionStateCreatedOpening:
            NSLog(@"FBSessionStateCreatedOpening");
            break;
            
        case FBSessionStateCreatedTokenLoaded:
            NSLog(@"Token Got!");
            //  even have vaild Token, still need a Session to do the communication.
            [FBSession openActiveSessionWithAllowLoginUI:NO];   //  create a session.
            
        case FBSessionStateOpen:
            NSLog(@"%@", (nowState==FBSessionStateOpen)?@"FBSessionStateOpen":@"FBSessionStateCreatedTokenLoaded");
            
            MainViewController *main = [[[MainViewController alloc] init] autorelease];
            [self presentViewController:main animated:YES completion:nil];
            break;
            
        case FBSessionStateOpenTokenExtended:
            NSLog(@"FBSessionStateOpenTokenExtended");
            break;
    }
}

@end


MainViewController.m

#import "MainViewController.h"
#import <FacebookSDK/FacebookSDK.h>

@interface MainViewController ()

@end

@implementation MainViewController

- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
{
    self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
    if (self) {
        // Custom initialization
    }
    return self;
}

- (void)viewDidLoad
{
    [super viewDidLoad];

}

- (void)dealloc {
    [_infoLabel release];
    [_showView release];
    [super dealloc];
}

- (void)viewDidUnload {
    [self setInfoLabel:nil];
    [self setShowView:nil];
    [super viewDidUnload];
}

- (void)didReceiveMemoryWarning
{
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}

- (IBAction)getInfo:(id)sender {
    NSLog(@"Connecting");
    
    [FBRequestConnection
     startForMeWithCompletionHandler:^(FBRequestConnection *connection,
                                       id<FBGraphUser> user,
                                       NSError *error) {
         NSLog(@"Response done.");
         if (!error) {
             NSMutableString *userInfo = [[[NSMutableString alloc] init] autorelease];
             NSLog(@"user : %@", user);
             // Example: typed access (name)
             // - no special permissions required
             [userInfo appendString: [NSString stringWithFormat:@"Name : %@\n\n",user.name]];

             // Example: typed access, (birthday)
             // - requires user_birthday permission
             [userInfo appendString: [NSString stringWithFormat:@"Birthday : %@\n\n",user.birthday]];
             
             // Example: partially typed access, to location field,
             // name key (location)
             // - requires user_location permission
             [userInfo appendString: [NSString stringWithFormat:@"Location : %@\n\n", user.location[@"name"]]];

             // Example: access via key (locale)
             // - no special permissions required
             [userInfo appendString: [NSString stringWithFormat:@"Locale : %@\n\n", user[@"locale"]]];

             // Example: access via key for array (languages)
             // - requires user_likes permission
             if (user[@"languages"]) {
                 NSArray *languages = user[@"languages"];
                 NSMutableArray *languageNames = [[NSMutableArray alloc] init];
                 for (int i = 0; i < [languages count]; i++) {
                     languageNames[i] = languages[i][@"name"];
                 }
                 [userInfo stringByAppendingString:
                        [NSString stringWithFormat:@"Languages: %@\n\n", languageNames]];
             }

             // Display the user info
             self.showView.text = userInfo;
             
         }else {
             NSLog(@"error : %@", error);
         }
     }];
    
}

- (IBAction)logoutAction:(id)sender {
    [FBSession.activeSession closeAndClearTokenInformation];
    [self dismissViewControllerAnimated:YES completion:nil];
}

@end

OK!Coding完你可能會問:


那...認證呢?

 好問題!見到LoginViewController.m登入按鈕裡用的

[FBSession openActiveSessionWithReadPermissions:allowLoginUI:completionHandler:]

方法了?

這個方法有三個參數:

1. Permission:要使用者授權讓我們可以讀取的項目,可以在這裡找到。

2. allowLoginUI:是否要跳出授權視窗?
    跳出的方法預設是:
    a. iOS6 Native Login Dialog(如果系統是iOS6 up,直接使用手機預設帳號)。
    b. Facebook App Native Login Dialog(如果手機有裝Facebook App,使用目前登錄的帳號)。
    c. Facebook Web Native Login Dialog(推出一個WebView,但每一次使用者都要進行登入)。
    d. 內建瀏覽器Safari(推出Safari,進入到Facebook認證網頁,很lol)。

    你可以在這裡找到詳細說明。

3. completionHandler:Block,認證不論有沒有成功,會執行。


這個方法會先建立一個Session,然後試著找有沒有可用的Token,
如果找到,state = FBSessionStateCreatedTokenLoaded,載入Token,不推出LoginUI,
否則會檢查allowLoginUI:推出LoginUI進行認證,
認證期間,state = FBSessionStateCreatedOpening,
認證成功,state = FBSessionStateOpen,
認證失敗,state = FBSessionStateClosedLoginFailed。

所以成功的完成認證。

 
注意,要完整跑完流程一次,記得要把模擬器重設以及Facebook帳號要刪掉App的授權。





那...有沒有另一種方法呢?

FBSession提供的靜態方法有openActiveSession...開頭的都可以玩看看,參考文件

也可以建立實體:
FBSession *session = [FBSession alloc],再選擇合適的init...方法,
利用實例方法openWith...開起Sesion,都可試試。
如果要自己設定LoginUI的方式,可以用openWithBehavior:completionHandler:,設定Behavior參數來達到。


那...資料怎麼抓的呢?

好問題!我們下一部曲再說吧!XD


App畫面:

 
如果你的Facebook帳號有設定登入碼認證,則會多一確認步驟。

 如果有開登入提醒的話然後會收到簡訊,像最後一條簡訊一樣。



Note :

a. 如果出現:
Error Domain=com.facebook.sdk Code=5 "The operation couldn’t be completed. (com.facebook.sdk error 5.)" UserInfo=0xa1580e0 {com.facebook.sdk:ParsedJSONResponseKey={
    body =     {
        error =         {
            code = 2500;
            message = "An active access token must be used to query information about the current user.";
            type = OAuthException;
        };
    };
    code = 400;
}, com.facebook.sdk:HTTPStatusCode=400}

表示讀到可用Token但沒有Session可用,讀到Token用FBSession建一個Session就好了。


b. 如果出現:
FBSDKLog: Cannot use the Facebook app or Safari to authorize, ?????? is not registered as a URL Scheme

App Secret無法辨識,如果用上面的??????把.plist的App Secret換掉就不會出現,但是LoginUI會變成用Safari開啓而不是WebView。


c. 如果出現:認證期間出現錯誤,可能a狀況出現太多次,暫時被擋掉,稍待一會就好了。



繼續前進:【教學】Facebook SDK 二部曲:從Facebook抓取資料




參考文件:

Understanding FBSessions

FBSession Reference

Permissions

Using Facebook Login


參考範例:

Fetch User Data for iOS

留言

  1. 一點小問題 也許是我認知不對~
    URL Schemes那邊不是設定App Secret, 應該是 "fb" + App ID

    另外最好提到AppDelegate那邊要設定handle openurl
    新手不知道沒設定的話, 如果不是在App內授權
    而是跳到Safari或是FB app授權, 返回會拿不到token

    回覆刪除
  2. 你是對的,感謝你提出,

    AppDelegate 需要設定handle openurl因加進去說明會太複雜,
    所以範例儘量都避免掉,會再補寫一篇來補充,

    感謝你!

    回覆刪除
  3. 不好意思,請問一下
    當我按下要同意授權的的完成後,要跳轉回APP的時候似乎不會做任何動作
    所以回到APP時我還是在第一頁
    我發現completionHandler的block要按第二次同意才會執行,這是不是怪怪的

    感謝大大解答

    回覆刪除
  4. 我試了試沒發現這種狀況,

    試著用實體的方法開啓認證看看,把原本loginButtonAction內註解掉換成:

    FBSession *session = [[FBSession alloc] initWithPermissions:self.permissions];

    [FBSession setActiveSession:session];

    [session openWithBehavior:FBSessionLoginBehaviorForcingWebView completionHandler:^(FBSession *session, FBSessionState status, NSError *error) {
    NSLog(@"status : %@", error?error:@"OK");
    [self checkState];
    }];

    另外可否提供測試的機型及程式碼,好讓我Debug一下?

    回覆刪除
  5. 感謝大大,用您新的程式碼替換後已經可以正常執行

    只是我還是不太瞭解原本的方法為什麼無法正常執行

    我的是用iphone 6.1模擬器run的,至於程式碼是copy您的哈哈哈哈,所以應該是一樣的
    是我沒有設定到什麼嗎?

    另外我main的那一頁似乎少你- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil 這個方法,這個是會自動產生的嗎?
    因為我第二頁有但是第一頁卻沒有

    最後還是謝謝大大了~~
    期待您出更多文章 :))

    回覆刪除
  6. 不好意思,我還有其他問題嗚嗚嗚
    關於第二頁,我發現 if (user[@"languages"]) 這個判斷式不會執行,if內的值似乎一直都是NO
    這樣是正常的嗎?

    謝大大

    回覆刪除
  7. 哎呀~這情況在我這試不出來,請問一下按第二次才出現,是一直都是這樣嗎?試著把模擬器Reset再試一次,還是得跟您要專案來看看,才比較完全,

    另initWithNibName那是自動產生的,用來讀取nib,不過沒有多作設定所以沒什麼關係,

    if (user[@"languages"]) ,如果登入的使用者沒有在"基本資料"中設定到這一項就不會回傳,且Token必須要有user_likes的權限,

    感謝的支持,

    不過最近在趕工作,可能會有點久才更新 :P ,請見諒!

    回覆刪除
  8. 關於程式,我有請學長幫我看看,後來有成功try出來了,不過他和你的方法幾乎不一樣,他是寫在AppDelegate內

    這幾天謝謝大大的幫忙,不好意思耽誤你的工作時間XD

    回覆刪除
  9. 抱歉打擾一下大大
    我是IOS開發的新手
    我照著大大您的步驟做 但是出現了三個錯誤
    不知道能否請傑大幫我看一下 謝謝

    出在LoginViewController.m

    Property 'permissions' not found on object of type 'LoginViewController *'
    Use of undeclared identifier '_permissions'
    Use of undeclared identifier '_permissions'

    上面這三個問題 我在h檔加了@property (readwrite, copy) NSArray *permissions;

    可以build過 但是一按下按鈕就停掉了 不知道是什麼問題
    請大大幫我看一下 謝謝

    回覆刪除
  10. 嗨!

    錯誤是沒加上property沒錯,
    請問是哪個按鈕?按下後就停掉是指...程式無誤卻沒動作?Crash?或其他的反應?
    可以再提供多點資訊像是錯誤訊息、中途有什麼反應?

    感謝!

    回覆刪除
  11. 大大您好 現在已經解決掉了

    目前getInfo也已經取得資料 但是現在就是如果我沒登出直接按HOME回桌面 然後按兩下HOME清除後

    再打開就會自動閃退

    錯誤訊息為

    2013-09-05 13:09:43.471 facebook[1079:c07] Token Got!
    2013-09-05 13:09:43.472 facebook[1079:c07] FBSessionStateCreatedTokenLoaded
    2013-09-05 13:09:43.482 facebook[1079:c07] *** Terminating app due to uncaught exception 'NSUnknownKeyException', reason: '[ setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key getInfo.'
    *** First throw call stack:
    (0x260a012 0x140ee7e 0x2692fb1 0xebae41 0xe3c5f8 0xe3c0e7 0xe66b58 0x570019 0x1422663 0x260545a 0x56eb1c 0x4337e7 0x433dc8 0x433ff8 0x434232 0x43fc25 0x63f3a3 0x43cee3 0x43d167 0x313b 0x2c74 0x436103 0x4364df 0x437c4c 0x4367ef 0x3692c0 0x369245 0x369095 0x25d2afe 0x25d2a3d 0x25b07c2 0x25aff44 0x25afe1b 0x28f57e3 0x28f5668 0x352ffc 0x27d2 0x2705)
    libc++abi.dylib: terminate called throwing an exception
    (lldb)


    因為我是用storyboard做畫面所以我有將LoginViewController的LoginButton跟MainViewController這個畫面做model的連結

    回覆刪除
  12. 抱歉我再補充一下 如果沒有做model連結

    facebook按登入後 會全部空白

    MainViewController *main = [[[MainViewController alloc] init] autorelease];
    [self presentViewController:main animated:YES completion:nil];
    break;

    似乎沒作用

    回覆刪除
  13. 你好~想請問一下
    我再LoginViewController.m
    - (void)checkState裡
    case FBSessionStateOpenTokenExtended:這行
    出現了switch case is protected scope 的錯誤
    請問該怎麼處理?我是使用xcode 5和3.8版本的sdk

    回覆刪除
  14. 可以po部分程式碼嗎?

    另外,如果在switch case 裡實體化一個物件,compiler會不知道屬於誰的,
    可以在之前就先設好,或是在case : 跟break;之間加入大括弧{ },再括弧裡寫就好了。

    回覆刪除
  15. - (IBAction)loginButtonAction:(id)sender {
    NSLog(@"check session and token is vaild :");
    // check if user already login or not
    // little bug at LoginUI in webview, you can close it by click left-up corner.
    NSArray *permissions = @[@"basic_info",
    @"user_location",
    @"user_birthday",
    @"user_likes"];

    [FBSession openActiveSessionWithReadPermissions:permissions
    allowLoginUI:YES
    completionHandler:^(FBSession *session,
    FBSessionState status,
    NSError *error) {
    // called while state changed

    [self checkState];

    }];
    }

    - (void)checkState{

    FBSessionState nowState = FBSession.activeSession.state;

    switch (nowState) {
    case FBSessionStateClosed:
    NSLog(@"FBSessionStateClosed");
    break;

    case FBSessionStateClosedLoginFailed:
    NSLog(@"FBSessionStateClosedLoginFailed");
    break;

    case FBSessionStateCreated:
    NSLog(@"FBSessionStateCreated");
    NSLog(@"No Login");
    break;

    case FBSessionStateCreatedOpening:
    NSLog(@"FBSessionStateCreatedOpening");
    break;

    case FBSessionStateCreatedTokenLoaded:
    NSLog(@"Token Got!");
    // even have vaild Token, still need a Session to do the communication.
    [FBSession openActiveSessionWithAllowLoginUI:NO]; // create a session.

    case FBSessionStateOpen:
    NSLog(@"%@", (nowState==FBSessionStateOpen)?@"FBSessionStateOpen":@"FBSessionStateCreatedTokenLoaded");

    MainViewController *main = [[MainViewController alloc] init];
    [self presentViewController:main animated:YES completion:nil];
    break;

    case FBSessionStateOpenTokenExtended:
    NSLog(@"FBSessionStateOpenTokenExtended");
    break;
    }
    }
    請問 以下 這個地方會有 switch case is protected scope 的錯誤 我有看到您的回答可是我看不懂意思
    case FBSessionStateOpenTokenExtended:
    NSLog(@"FBSessionStateOpenTokenExtended");
    break;

    回覆刪除
  16. 你可以像這樣:

    UIViewController *next = nil;
    switch (number) {
    case 1:
    next = [[ViewController alloc] init];
    [self presentViewController:next animated:YES completion:nil];
    break;

    case 2:
    next = [[MainViewController alloc] init];
    [self presentViewController:next animated:YES completion:nil];
    break;
    }

    或是這樣(Case 裡用大括弧):

    switch (number) {
    case 1:
    {
    MainViewController *main = [[MainViewController alloc] init];
    [self presentViewController:main animated:YES completion:nil];
    }
    break;

    case 2:
    {
    NextViewController *next = [[NextViewController alloc] init];
    [self presentViewController:main animated:YES completion:nil];
    }
    break;
    }

    回覆刪除
  17. "表示讀到可用Token但沒有Session可用,讀到Token用FBSession建一個Session就好了。"
    這段解釋有點不懂,跟A情況一模一樣的問題。
    可以詳細一點的解釋嘛?

    回覆刪除

張貼留言

這個網誌中的熱門文章

【給程式新手】陰魂不散的物件導向?

【教學】Facebook SDK 前傳:準備Social一下