Pregunta

I’m testing code based on the firechat-ios example. I’ve added the FirebaseSimpleLogin call loginToFacebookAppWithId and have it set up so that one view controller performs the login and then transitions to a different view controller that holds the chat logic:

self.firebase = [[Firebase alloc] initWithUrl:@"https://xxxxx.firebaseio.com/"];

[self observeEventType:FEventTypeChildAdded withBlock:^(FDataSnapshot *snapshot) {
    NSLog(@"%@", snapshot.value);

    // Add the chat message to the array.
    [self.chat addObject:snapshot.value];
    // Reload the table view so the new message will show up.
    [self.tableView reloadData];

    [self.tableView scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:([self.tableView numberOfRowsInSection:0] - 1) inSection:0] atScrollPosition:UITableViewScrollPositionBottom animated:YES];
}];

FirebaseSimpleLogin *authClient = [[FirebaseSimpleLogin alloc] initWithRef:self.firebase];

[authClient loginToFacebookAppWithId:kFacebookAppID permissions:@[@"email"]
            audience:ACFacebookAudienceOnlyMe
            withCompletionBlock:^(NSError *error, FAUser *user) {

    if (error != nil) {
        // There was an error logging in
        NSLog(@"facebook error");
    } else {
        // We have a logged in facebook user
        NSLog(@"facebook logged in");

        [authClient checkAuthStatusWithBlock:^(NSError* error, FAUser* user) {
            if (error != nil) {
    // Oh no! There was an error performing the check
    NSLog(@"auth error");
            } else if (user == nil) {
    // No user is logged in
    NSLog(@"auth not logged in");
            } else {
    // There is a logged in user
    NSLog(@"auth logged in");

    // segue to the chat view controller
    [self performSegueWithIdentifier:@"segueToViewController" sender:self];
            }
        }];
    }
}];

Here are the firebase rules:

{
    "rules": {
        ".read": "auth != null",
        ".write": "auth != null"
    }
}

The problem is, about 10% of the time, the UITableView of the chat messages is blank, and I don’t see any chat message entries in the log. I’ve tried playing around with the order of observeEventType, putting it before and after the loginToFacebookAppWithId call.

I’m wondering if there is a race condition where maybe the messages are arriving before I call observeEventType. I’ve checked the return value of observeEventType and I get a FirebaseHandle of 1 even when no messages arrive. I’ve also upgraded the firebase framework that comes with firechat ios to https://cdn.firebase.com/ObjC/Firebase.framework-LATEST.zip and it still fails.

I thought that maybe the connection dropped, but I’m able to post messages with childByAutoId after I’ve authenticated and see them appear on the firebase server. I just never receive any messages.

I wonder if it’s trying to send me the messages in the brief moment before I’m authenticated, and failing because I don’t have read permission. Is there a way to delay event observations until after I’m in?

I’ve tried everything I can think of but I can’t make it work reliably.

---------- UPDATE ----------

I seem to be able to log in every time if I type my credentials manually. I'm currently checking for a previous successful login with:

[FBSession openActiveSessionWithAllowLoginUI:false]

To determine if I successfully logged in on the last launch of the app. If it fails, I go to a view controller for FirebaseSimpleLogin. But if it works, I call FirebaseSimpleLogin in the current view controller and wait till it succeeds in the background.

I'm running in the simulator, so I tried deleting the preferences plist at:

~/Library/Application Support/iPhone Simulator/7.0.3/Applications/XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX/Library/Preferences/com.xxxxxxxxxx.plist

and relaunching, which forces me to re-authenticate. Then I tried typing in my credentials and logging in 25 times without a problem.

So I think the problem is either somehow related to trying to login with Facebook before I use FirebaseSimpleLogin, or logging in with credentials from the previous launch (without bringing up the login dialog). I'm still trying to narrow down the culprit.

---------- UPDATE 2 ----------

I just wanted to add a note that after further testing, I found that the call to:

[FBSession openActiveSessionWithAllowLoginUI:false]

has no effect on FirebaseSimpleLogin. If I skip that call altogether and simply substitute true or false there, I can reproduce the issue. The problem turned out to be a race condition, see my answer below.

¿Fue útil?

Solución

I finally figured out what was happening, it was due a wrong assumption on my part about UIViewController message callbacks and CFRunLoop.

The code sample in my question was distilled down from my real code to remove extraneous calls, but it turns out the part I removed was actually the culprit. I had written a function to log in and wait until success or failure on the spot (rather than receiving the response in a block later) by using a run loop:

-(bool)loginUsingFacebookReturningError:(NSError**)error andUser:(FAUser**)user
{
    __block NSError *errorTemp;
    __block FAUser  *userTemp;

    [self loginUsingFacebookWithCompletionBlock:^(NSError *error, FAUser *user) {
        errorTemp = error;
        userTemp = user;

        CFRunLoopStop(CFRunLoopGetCurrent());
    }];

    CFRunLoopRun(); // needs a timeout or way for the user to cancel but I haven't implemented it yet

    if(error) *error = errorTemp;
    if(user) *user = userTemp;

    return !errorTemp && userTemp;
}

-(void)loginUsingFacebookWithCompletionBlock:(void (^)(NSError* error, FAUser* user))block
{
    FirebaseSimpleLogin *authClient = [[FirebaseSimpleLogin alloc] initWithRef:self.firebase];

    [authClient loginToFacebookAppWithId:kFacebookAppID permissions:@[@"email"]
        audience:ACFacebookAudienceOnlyMe
         withCompletionBlock:^(NSError *error, FAUser *user) {

             if (error != nil) {
                 // There was an error logging in
                 NSLog(@"facebook error");

                 block(error, nil);
             } else {
                 // We have a logged in facebook user
                 NSLog(@"facebook logged in");

                 [authClient checkAuthStatusWithBlock:block];
             }
         }];
}

This was called with:

NSError *error;
FAUser  *user;
bool    success = [self loginUsingFacebookReturningError:&error andUser:&user];

The way loginUsingFacebookReturningError works is, it calls loginUsingFacebookWithCompletionBlock which fires off the loginToFacebookAppWithId and checkAuthStatusWithBlock messages like usual, but then I start a run loop. The run loop allows processing to happen in the background, even though the main thread pauses on CFRunLoopRun() until the completion block calls CFRunLoopStop().

What I hadn't realized is that run loops continue to process the application's messages in the background. So while I thought program flow had stopped in viewDidLoad, it had actually continued and called viewWillAppear, which is where I had placed my call to observeEventType (because I assumed that authentication would be complete by the time the program got there).

This created a race condition where the program attached the observeEventType callback during the time that Facebook and Firebase were authenticating. 90% of the time, authentication had completed before observeEventType was called, but 10% of the time there was lag or other network delays and observeEventType was called prematurely.

I fixed the problem by moving the FirebaseSimpleLogin code to its own view controller in the storyboard, and using the completion block to initiate the segue to the next view controller, which installed the observeEventType callback.

So to summarize: the solution is to call FirebaseSimpleLogin's authentication, and then AFTER it has finished and the completion block is done, call observeEventType. Otherwise Firebase's rules will deny your request to see data that's only visible to authenticated users (which is correct).

Here is the final code, untested but the method works:

// only global for illustration purposes, should really go in a singleton or AppDelegate, or be passed through the segue to the next view controller
Firebase    *gFirebase;

// LoginViewController (root view controller in storyboard)
- (void)viewDidLoad
{
    [super viewDidLoad];

    gFirebase = [[Firebase alloc] initWithUrl:@"https://xxxxx.firebaseio.com/"];

    FirebaseSimpleLogin *authClient = [[FirebaseSimpleLogin alloc] initWithRef:gFirebase];

    [authClient loginToFacebookAppWithId:kFacebookAppID permissions:@[@"email"]
                audience:ACFacebookAudienceOnlyMe
                withCompletionBlock:^(NSError *error, FAUser *user) {

        if (error != nil) {
            // There was an error logging in
            NSLog(@"facebook error");
        } else {
            // We have a logged in facebook user
            NSLog(@"facebook logged in");

            [authClient checkAuthStatusWithBlock:^(NSError* error, FAUser* user) {
                if (error != nil) {
                    // Oh no! There was an error performing the check
                    NSLog(@"auth error");
                } else if (user == nil) {
                    // No user is logged in
                    NSLog(@"auth not logged in");
                } else {
                    // There is a logged in user
                    NSLog(@"auth logged in");

                    // segue to the chat view controller
                    [self performSegueWithIdentifier:@"segueToViewController" sender:self];
                }
            }];
        }
    }];
}

// ViewController (destination of segueToViewController)
- (void)viewDidLoad
{
    [super viewDidLoad];

    [gFirebase observeEventType:FEventTypeChildAdded withBlock:^(FDataSnapshot *snapshot) {
        NSLog(@"%@", snapshot.value);

        // Add the chat message to the array.
        [self.chat addObject:snapshot.value];
        // Reload the table view so the new message will show up.
        [self.tableView reloadData];

        [self.tableView scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:([self.tableView numberOfRowsInSection:0] - 1) inSection:0] atScrollPosition:UITableViewScrollPositionBottom animated:YES];
    }];
}
Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top