iOS Authentication Blocks

In my recent project, Cakewalk, we give the user a fairly complete experience without having to sign up. This seems like the nice thing to do but it’s also in Apple’s guidelines for app approval. At some point, though, we need to create an account for them. In our case, it’s when they want to add an idea to their favorites or save their filter preferences. A “gate” such as this is something I’ve done on the web many times but it’s the first time I’ve done it an iPhone app. I’ll describe how I solved it using blocks.

Web

On the web, I would usually send them to a URL. If that URL requires a logged in user, then it would redirect to the login/signup URL with a query param. For example, if the page was http://www.example.com/favorite, it would redirect to something like http://www.example.com/login?after_auth=%2Ffavorite. The login/signup form would add this as a hidden parameter that it POSTs. If the action is successful, it will use this url instead of the default one to send the user to next place. In Rails, it would look something like this:

1
2
3
4
5
6
7
8
9
10
11
def login
  user = User.find_by_email(params[:email])
  if user.authenticate(params[:password])
    # right!
    log_in!(user)
    redirect_to params[:after_auth] || dashboard_path
  else
    # not right!
    render :form
  end
end

If the site allows the users to switch back and forth between login and signup pages or allows Facebook, Twitter or some other OAuth, it can be easy to lose this parameter. I’ve seen sites put the value in a cookie for this reason. It can simplify it, but is more likely to have unintended site effects.

Blocks

Most of the complexity of the web version is because of the stateless nature of the medium. The query parameter or cookie is an attempt to add that state globally. This is not particularly an issue in an iPhone app. The global state is fairly well known in the view controllers and there is shared memory. The interesting thing that I realized is that it’s not just data that we can store, but also blocks.

An Objective C block is an inline declaration of a function. It is similar to features available in Ruby and Javascript. Here we use one to change how a sort is done:

1
2
3
4
5
6
NSComparator finderSortBlock = ^(id string1, id string2) {
    NSRange string1Range = NSMakeRange(0, [string1 length]);
    return [string1 compare:string2 options:comparisonOptions range:string1Range locale:currentLocale];
};

NSArray *finderSortArray = [stringsArray sortedArrayUsingComparator:finderSortBlock];

I find it a little hard to read when using them like this, so I typedef’d them as needed:

1
2
3
4
5
6
// blocks.h
typedef void (^MyVoidBlock)();
typedef void (^MyVoidBlockWithBool)(BOOL);
typedef void (^MyVoidBlockWithString)(NSString *);
typedef void (^MyVoidBlockWithDictionary)(NSDictionary *);
typedef BOOL (^MyBoolBlockWithString)(NSString *);

This allows us to use them throughout the app to provide more semantics to the block in use. For example, in a method declaration:

1
2
3
4
5
6
// MyAppDelegate.h
#import <UIKit/UIKit.h>

@interface MyAppDelegate : UIResponder <UIApplicationDelegate>
- (void) authenticateWithSuccess:(MyVoidBlock)success;
@end

Use in context

So most of the time, it does not matter if the user is logged in or not, but then the time comes they hit the “favorite” button and we need to make sure.

1
2
3
4
5
6
7
// MyIdeaViewController.m
-(void) favoriteButtonPressed: {
    MyAppDelegate * delegate = (MyAppDelegate*) [[UIApplication sharedApplication] delegate];
    [delegate authenticateWithSuccess:^{
        [[CurrentUser shared] setIdeaAsFavorite: self.idea];
    }];
}

This was the cleanest of several alternatives that I came up with. It just occurred to me, though, that I also could have made a block that passed a user instead of nothing for even better encapsulation.

1
2
3
4
5
6
7
8
9
10
11
12
13
// blocks.h
typedef void (^MyVoidBlockWithUser)(CurrentUser *);

// MyAppDelegate.h
- (void) authenticateWithSuccess:(MyVoidBlockWithUser)success;

// MyIdeaViewController.m
-(void) favoriteButtonPressed: {
    MyAppDelegate * delegate = (MyAppDelegate*) [[UIApplication sharedApplication] delegate];
    [delegate authenticateWithSuccess:^(CurrentUser* user) {
        [user setIdeaAsFavorite: self.idea];
    }];
}

I did the earlier version because the app already had that shared user concept. Either way, this technique encapsulates all the logic for getting the user authenticated and lets the feature focus on what happens if it is successful (or already logged in).

Callbacks

The system works because the block is passed along much like the web parameter.

1
2
3
4
5
6
7
8
9
10
11
12
13
// MyAppDelegate.m
- (void) authenticateWithSuccess:(MyVoidBlock)success {
    if ([CurrentUser isLoggedIn]) {
        // return immediately if the user is already logged in
        success();
    }
    else {
        // otherwise, launch view that allows login or signup, passing block along
        MyAuthController * auth = [[MyAuthController alloc] initWithSuccess:success];
        auth.modalTransitionStyle = UIModalTransitionStyleCoverVertical;
        [self.rootViewController presentViewController:auth animated:YES completion:nil];
    }
}

This slides up the authentication controller and most importantly passes along the success callback. Then, if the user does login or signup, the auth controller closes itself, logs the user in, and calls the success block.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// MyAuthController.m
-(void) didSubmitLogin {
    [CurrentUser loginWithEmail:self.emailField.text password:self.passwordField.text success:^{
        [self closeAuthentication:YES];
    } failure:^(NSError *error) {
        [self showError:error];
    }];
}
-(void) didTapCancelButton {
    [self closeAuthentication:NO];
}
- (void) closeAuthentication:(BOOL)authenticated {
    [self dismissViewControllerAnimated:YES completion:^{
        if (authenticated) {
            self.successBlock();
        }
    }];
}

If they do hit the cancel, button, it will slide down and everything will be back where it started. However, if they do login, what I think is really cool about this is that it slides down and the originally invoked code is back in the same position as if the user would have been authenticated in the first place.

Summary

I hadn’t used blocks much in Objective C before, so this ability was a new concept to me. The power of it lies in the simplicity of the favoriteButtonPressed code even in the face of handling several varying circumstances. I’m sure there are other cases like this so it’s nice to have a new tool in the toolbelt.

Discuss on Hacker News

Comments

Copyright © 2017 Brian Leonard