你如何打开一个带有完成处理程序的 FBSession,它不会被保留并在每次会话状态更改时调用?

Posted

技术标签:

【中文标题】你如何打开一个带有完成处理程序的 FBSession,它不会被保留并在每次会话状态更改时调用?【英文标题】:How do you open a FBSession with a completion handler that does not get retained and called on every session state change? 【发布时间】:2013-03-27 15:55:00 【问题描述】:

出于某种奇怪的原因,Facebook 决定创建实际上只是通知观察者的完成处理程序。忽略他们为什么这样做,我需要一些方法来请求打开一个 FBSession 并且只发生一次回调,因为回调中的对象不能被保留。

如果用户按下按钮并且具有“publish_actions”权限的 Facebook 会话未打开,则应显示活动指示器并防止在 Facebook 会话打开时与 UI 交互。

这是用于登录 Facebook 的方法:

- (void)loginToFacebookFromViewController:(UIViewController *)viewController completion:(void (^)(NSString *facebookAccountName))completion

    if([self isLoggedIntoFacebook] == NO)
    
        [viewController startLoadingAnimation];

        [FBSession openActiveSessionWithPublishPermissions: @[ @"publish_actions" ] defaultAudience:FBSessionDefaultAudienceFriends allowLoginUI:YES completionHandler:^(FBSession *session, FBSessionState status, NSError *error)
            
                if(error == nil)
                
                    [FBRequestConnection startForMeWithCompletionHandler:^(FBRequestConnection *connection, id<FBGraphUser> user, NSError *error)
                        
                            NSString *accountName = [user name];

                            if(error == nil)
                            
                                NSUserDefaults *standardUserDefaults = [NSUserDefaults standardUserDefaults];
                                [standardUserDefaults setObject:accountName forKey:TSFacebookAccountName];
                                [standardUserDefaults synchronize];
                            

                            [viewController stopLoadingAnimation];

                            if(completion != nil)
                                completion(accountName);
                        ];
                
                else
                
                    [viewController stopLoadingAnimation];

                    if(completion != nil)
                        completion(nil);
                
            ];
    
    else
    
        if(completion != nil)
            completion([self facebookAccountName]);
    

如果没有保留完成处理程序(再次出于毫无意义的原因,因为会话状态更改时会发布 NSNotifications),那么此方法将完美运行。

由于该应用程序不围绕 Facebook,我对会话状态何时更改没有任何兴趣。如果用户尝试向 Facebook 分享内容并且活动会话未打开,则登录过程将被启动,如果会话打开,则共享过程将继续。否则,将向用户显示一条错误消息。

有人能解释一下 Facebook 是如何期望实现这种功能的吗?呈现给用户以收集用于 Open Graph 帖子的特定信息。

【问题讨论】:

【参考方案1】:

如果您不像我一样关心从 FBSessionStateHandler 接收状态更改,那么下面的代码应该允许您登录 Facebook 并为您提供未保留的完成块。

当任何状态发生变化时,传递给方法的完成块总是会被使用,然后立即 nil'd ,这样如果发生另一个状态变化,它就会被忽略。

typedef void(^FacebookLoginCompletionBlock)(id<FBGraphUser> user);

- (void)loginToFacebookFromWithCompletionBlock:(FacebookLoginCompletionBlock)completionBlock

    // If the user is not logged into Facebook attempt to open a session with "publish_actions" permissions.
    if([[FBSession activeSession] isOpen] == NO)
    
        // Copy the completion block to a local variable so it can be nil'd out after it is used to prevent retain loops.
        __block FacebookLoginCompletionBlock copiedCompletionBlock = completionBlock;

        [FBSession openActiveSessionWithPublishPermissions:@[ @"publish_actions" ] defaultAudience:FBSessionDefaultAudienceFriends allowLoginUI:YES completionHandler:^(FBSession *session, FBSessionState status, NSError *error)
            
                // Only attempt to run any of this code if there is a completion block to call back to. If completion block is nil than it has already been used and this is a state change that we do not care about.
                if(copiedCompletionBlock != nil)
                
                    // Because this method is only concerned with the user logging into Facebook just worry about the open state occuring with no errors.
                    if(status == FBSessionStateOpen && error == nil)
                    
                        // If the user successfully logged into Facebook download their basic profile information so the app can save the information to display to the user what account they are logged in under.
                        [FBRequestConnection startForMeWithCompletionHandler:^(FBRequestConnection *connection, id<FBGraphUser> user, NSError *error)
                            
                                if(copiedCompletionBlock != nil)
                                    copiedCompletionBlock(user);

                                // nil out the copied completion block so it is not retained and called everytime the active FBSession's state changes.
                                copiedCompletionBlock = nil;
                            ];
                    
                    // Because this method is only concerned with the user logging into Facebook if any other state is triggered call the completion block indicating that there was a failure.
                    else
                    
                        if(copiedCompletionBlock != nil)
                            copiedCompletionBlock(nil);

                        // nil out the copied completion block so it is not retained and called everytime the active FBSession's state changes.
                        copiedCompletionBlock = nil;
                    
                

                // This block will exist the lifespan of the application because for some bizarre reason Facebook retains the completion handler for their open active session methods. Throw in some code that will display an error to the user if any session state changes occur that Facebook thinks the user should be aware of. Your code should be always checking if a active Facebook session exists before taking any action so not being aware of these changes should not be any issue. Worst case scenario you can listen for FBSessionDidSetActiveSessionNotification, FBSessionDidUnsetActiveSessionNotification, FBSessionDidBecomeOpenActiveSessionNotification or FBSessionDidBecomeClosedActiveSessionNotification notifications.
                if ([error fberrorShouldNotifyUser] == YES)
                
                    NSString *alertTitle = @"Error logging into Facebook";
                    NSString *alertMessage = [error fberrorUserMessage];

                    if ([alertMessage length] == 0)
                        alertMessage = @"Please try again later.";

                    UIAlertView *alertView =  [[UIAlertView alloc] initWithTitle:alertTitle message:alertMessage delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil];

                    [alertView show];
                
            ];
    
    // If the user is already logged into Facebook immediately call the completion block with the user object that should have been saved when the user previously logged in.
    else
    
        if(completionBlock != nil)
            completionBlock([self facebookUser]);
    

【讨论】:

【参考方案2】:

我实现类似行为的方式是将我的会话逻辑放在一个单例中。单例实现了一个完成处理程序,当会话发生变化时,FB 可以安全地调用该处理程序。然后,我的完成处理程序会发布自己的通知,以供其他控制器根据需要进行观察。

假设您有一个MySession 单例,您在其中定义了一些关于 FB 身份验证的方法和通知。您的视图控制器逻辑类似于:

- (void)viewDidLoad

    [super viewDidLoad];
    // observe MySession notifications
    [MySession.sharedSession addObserver:self];


- (void)dealloc

    // stop observing MySession notifications
    [MySession.sharedSession removeObserver:self];


// this is a user action that requires FB auth
- (void)someActionRequiringFacebookAuth

    if (![MySession.sharedSession isLoggedIntoFacebook]) 
        self.postAuthAction = @selector(someActionRequiringFacebookAuth);
        [MySession.sharedSession loginToFacebook];
    

    else 
        // perform action
    


// notification posted by MySession before a FB auth attempt
- (void)mySessionWillAttemptFacebookAuth:(NSNotification *)notification

    [self startLoadingAnimation];


// notification posted by MySession if a FB auth attempt succeeds
- (void)mySessionDidAuthWithFacebook:(NSNotification *)notification

    [self stopLoadingAnimation];
    if (self.postAuthAction) 
        [self performSelector:self.postAuthAction];
        self.postAuthAction = nil;
    


// notification posted by MySession if a FB auth attempt fails
- (void)mySessionFacebookAuthDidFail:(NSNotification *)notification

    [self stopLoadingAnimation];
    // display error if desired
    self.postAuthAction = nil;

【讨论】:

我更喜欢不需要多个方法声明和添加/删除观察者调用的解决方案。为简单起见,我正在寻找一种在 someActionRequiringFacebookAuth 方法中处理所有逻辑的方法。

以上是关于你如何打开一个带有完成处理程序的 FBSession,它不会被保留并在每次会话状态更改时调用?的主要内容,如果未能解决你的问题,请参考以下文章

如何制作一个 nil 兼容的完成处理程序?

如何利用批处理命令让两个WINDOWS程序按照顺序运行?

如何使用完成处理程序和参数调用函数

如何在完成处理程序中使用返回值?

NodeJS 未退出,如何找到打开的处理程序?

带有完成处理程序输出的 PassthroughSubject?