sábado, 6 de noviembre de 2010

Facebook Like Button on iOS

This post has been permanently moved to http://angelolloqui.com/blog/10-Facebook-Like-Button-on-iOS


The problem

Some days ago a client asked us for including a Like Facebook button in one of his iPad applications. We have previously used Facebook iOS SDK (https://github.com/facebook/facebook-ios-sdk) for including things like the user's profile photo, friends, and so on so we were pretty sure that this button would be easy to implement.
Upppssss, what an error! Facebook iOS API doesn't include a FB Like button, and the Rest API either. The only way that Facebook seems to give to developers is a HTML button or iframe, both of them thinked for being in a web enviroment. Of course we have the chance to include a webview in the iPad app to include this button, but we should take care of the login process and some other issues, so I did some research and I found this:

http://petersteinberger.com/2010/06/add-facebook-like-button-with-facebook-connect-iphone-sdk/

This web has the solution that I was looking for, but, after including it's code (with a minor change due to a miss method), it didn't work as expected. The FB login dialog opens and then immediatly closes.

The solution
Here are the changes and improvements I have done for resolving it, with the complete code:

First, we have to customize a FBDialog for taking care of the login process:

//FBCustomLoginDialog.h
@interface FBCustomLoginDialog : FBDialog {
}
@end



//FBCustomLoginDialog.m
@implementation FBCustomLoginDialog

- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request
navigationType:(UIWebViewNavigationType)navigationType {
NSURL* url = request.URL;
if ([[url absoluteString] rangeOfString:@"login"].location==NSNotFound) {
[self dialogDidSucceed:url];
return NO;
}else if (url!=nil){
[_spinner startAnimating];
[_spinner setHidden:NO];
return YES;
}
return NO;
}
@end


As you may see, I changed the method "containsString" by "rangeOfString" because the first doesn't exists on iOS SDK. Another minor change I made is that I included a few sentences for start the spinner animation after the user submits the login form, and I had to put and extra "if" because sometimes the URL is null and it can't be loaded.

OK, so now we have a customized login view, but we need to make the button view for the "I like" UI. In order to build this view, my first option was to inherit my view directly from UIWebView, but after a while I decided to use a UIView over it because it gives me the chance to disable the webview scroll (the webpage generated for the FB Like button usually is heigher that the view itself, resulting in an awful scroll).
I also made some additions for customizing a little the colors showed in the webview, but I found a problem because the CSS of the page are loaded asynchronously with AJAX, so I can't know the exact moment in which I have to inject my javascript that changes the colors. This may be improved overriden the AJAX onreadystate to do it at the exact moment, but I didn't have so much time to investigate on this to only change a color, so I finally took the easy way, injecting the javascript a while after the page loads (3 secs in the code below). If you find a better solution for this I would appreciate if you post your code :)

Anyway, this is my final code:


//FBLikeButton.h
#define FB_LIKE_BUTTON_LOGIN_NOTIFICATION @"FBLikeLoginNotification"

typedef enum {
FBLikeButtonStyleStandard,
FBLikeButtonStyleButtonCount,
FBLikeButtonStyleBoxCount
} FBLikeButtonStyle;

typedef enum {
FBLikeButtonColorLight,
FBLikeButtonColorDark
} FBLikeButtonColor;

@interface FBLikeButton : UIView {

UIWebView *webView_;

UIColor *textColor_;
UIColor *linkColor_;
UIColor *buttonColor_;
}

@property(retain) UIColor *textColor;
@property(retain) UIColor *linkColor;
@property(retain) UIColor *buttonColor;

- (id)initWithFrame:(CGRect)frame andUrl:(NSString *)likePage andStyle:(FBLikeButtonStyle)style andColor:(FBLikeButtonColor)color;
- (id)initWithFrame:(CGRect)frame andUrl:(NSString *)likePage;

@end



//FBLikeButton.m
//LoginDialog es estatica para abrir unicamente un login en toda la app
static FBDialog *loginDialog_;

@implementation FBLikeButton

@synthesize textColor=textColor_, buttonColor=buttonColor_, linkColor=linkColor_;

- (id)initWithFrame:(CGRect)frame andUrl:(NSString *)likePage andStyle:(FBLikeButtonStyle)style andColor:(FBLikeButtonColor)color{
if ((self = [super initWithFrame:frame])) {
NSString *styleQuery=(style==FBLikeButtonStyleButtonCount? @"button_count" : (style==FBLikeButtonStyleBoxCount? @"box_count" : @"standard"));
NSString *colorQuery=(color==FBLikeButtonColorDark? @"dark" : @"light");

NSString *url =[NSString stringWithFormat:@"http://www.facebook.com/plugins/like.php?layout=%@&show_faces=true&width=%d&height=%d&action=like&colorscheme=%@&href=%@",
styleQuery, (int) frame.size.width, (int) frame.size.height, colorQuery, likePage];

//Creamos una webview muy alta para evitar el scroll interno por la foto del usuario y otras cosas
webView_ = [[UIWebView alloc] initWithFrame:CGRectMake(0, 0, frame.size.width, 300)];
[self addSubview:webView_];
[webView_ loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:url]]];
webView_.opaque = NO;
webView_.backgroundColor = [UIColor clearColor];
webView_.delegate = self;
webView_.autoresizingMask = UIViewAutoresizingFlexibleWidth;
[[webView_ scrollView] setBounces:NO];
self.backgroundColor=[UIColor clearColor];
self.clipsToBounds=YES;

[[NSNotificationCenter defaultCenter] addObserver:webView_ selector:@selector(reload) name:FB_LIKE_BUTTON_LOGIN_NOTIFICATION object:nil];

}
return self;
}

- (id)initWithFrame:(CGRect)frame andUrl:(NSString *)likePage{
return [self initWithFrame:frame andUrl:likePage andStyle:FBLikeButtonStyleStandard andColor:FBLikeButtonColorLight];
}

- (void)dealloc {

[[NSNotificationCenter defaultCenter] removeObserver:webView_ name:FB_LIKE_BUTTON_LOGIN_NOTIFICATION object:nil];

[webView_ stopLoading];
webView_.delegate=nil;
[webView_ removeFromSuperview];
[webView_ release]; webView_=nil;

self.linkColor=nil;
self.textColor=nil;
self.buttonColor=nil;

[super dealloc];
}


- (void) configureTextColors{
NSString *textColor=[textColor_ hexStringFromColor];
NSString *buttonColor=[buttonColor_ hexStringFromColor];
NSString *linkColor=[linkColor_ hexStringFromColor];

NSString *javascriptLinks = [NSString stringWithFormat:@"{"
"var textlinks=document.getElementsByTagName('a');"
"for(l in textlinks) { textlinks[l].style.color='#%@';}"
"}", linkColor];

NSString *javascriptSpans = [NSString stringWithFormat:@"{"
"var spans=document.getElementsByTagName('span');"
"for(s in spans) { if (spans[s].className!='liketext') { spans[s].style.color='#%@'; } else {spans[s].style.color='#%@';}}"
"}", textColor, (buttonColor==nil? textColor : buttonColor)];

//Lanzamos el javascript inmediatamente
if (linkColor)
[webView_ stringByEvaluatingJavaScriptFromString:javascriptLinks];
if (textColor)
[webView_ stringByEvaluatingJavaScriptFromString:javascriptSpans];

//Programamos la ejecucion para cuando termine
if (linkColor)
[webView_ stringByEvaluatingJavaScriptFromString:[NSString stringWithFormat:@"setTimeout(function () %@, 3000)", javascriptLinks]];
if (textColor)
[webView_ stringByEvaluatingJavaScriptFromString:[NSString stringWithFormat:@"setTimeout(function () %@, 3000)", javascriptSpans]];

}

///////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark -
#pragma mark UIWebViewDelegate

- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType {

if (loginDialog_!=nil)
return NO;

// if user has to log in, open a new (modal) window
if ([[[request URL] absoluteString] rangeOfString:@"login.php"].location!=NSNotFound){
loginDialog_= [[[FBCustomLoginDialog alloc] init] autorelease];
[loginDialog_ loadURL:[[request URL] absoluteString] get:nil];
loginDialog_.delegate = self;
[loginDialog_ show];
[loginDialog_.delegate retain]; //Retenemos el boton que ha abierto el login para que pueda recibir la confirmacion correctamente
return NO;
}
if (([[[request URL] absoluteString] rangeOfString:@"/connect/"].location!=NSNotFound) || ([[[request URL] absoluteString] rangeOfString:@"like.php"].location!=NSNotFound)){
return YES;
}

NSLog(@"URL de Facebook no contemplada: %@", [[request URL] absoluteString]);

return NO;
}

- (void)webViewDidFinishLoad:(UIWebView *)webView{

[self configureTextColors];
}

///////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark -
#pragma mark Facebook Connect

- (void)dialogDidSucceed:(FBDialog*)dialog {
[loginDialog_.delegate release];
loginDialog_.delegate=nil;
loginDialog_=nil;

//Lanzamos la notificacion para que se actualicen los botones
[[NSNotificationCenter defaultCenter] postNotificationName:FB_LIKE_BUTTON_LOGIN_NOTIFICATION object:nil];
}


/**
* Called when the dialog succeeds and is about to be dismissed.
*/
- (void)dialogDidComplete:(FBDialog *)dialog{
[self dialogDidSucceed:dialog];
}

/**
* Called when the dialog succeeds with a returning url.
*/
- (void)dialogCompleteWithUrl:(NSURL *)url{
[self dialogDidSucceed:loginDialog_];
}

/**
* Called when the dialog get canceled by the user.
*/
- (void)dialogDidNotCompleteWithUrl:(NSURL *)url{
[self dialogDidSucceed:loginDialog_];
}

/**
* Called when the dialog is cancelled and is about to be dismissed.
*/
- (void)dialogDidNotComplete:(FBDialog *)dialog{
[self dialogDidSucceed:loginDialog_];
}

/**
* Called when dialog failed to load due to an error.
*/
- (void)dialog:(FBDialog*)dialog didFailWithError:(NSError *)error{
[self dialogDidSucceed:loginDialog_];
}

@end


You may notice that I added a lot of code and methods to the original example. Almost all of them are for customizing the UI and make a proper use of our customized login dialog (the FBDialogDelegate methods).
The only important changes are the use of the NSNotificationCenter for sending notifications when the dialog success (and so every existing Like button could refresh with the new credentials) and the static reference to the FBCustomDialog to avoid multiple login popups at the same time.

The UIColor extensions used on the code above can be found here:
http://arstechnica.com/apple/guides/2009/02/iphone-development-accessing-uicolor-components.ars

Finally, you would see a method that doesn't exists on a standard UIWebView:
[[webView_ scrollView] setBounces:NO];

this method is a little hack I use sometimes to disable the scroll or bounces of the webview, but it may fail on future iOS versions. Anyway, if you still want to use it, this is what it does (I have it in a UIWebView category)

- (UIScrollView *) scrollView{
NSArray *subviews = [self subviews];
for (UIView *view in subviews){
if ([view isKindOfClass:[UIScrollView class]])
return (UIScrollView *) view;
}
return nil;
}



And thats all! with these 2 classes you are able to include your FB Like buttons in your views in an easy way, just with something like the following:

FBLikeButton *likeButton = [[FBLikeButton alloc] initWithFrame:frame andUrl:@"www.mylikeurl.com"];
[view addSubview:likeButton];
[likeButton release];

or even with customized colors:

FBLikeButton *likeButton = [[FBLikeButton alloc] initWithFrame:frame andUrl:@"www.mylikeurl.com"];
[likeButton setTextColor:COLOR_DARK_GRAY];
[likeButton setLinkColor:COLOR_CLEAR_GRAY];
[view addSubview:likeButton];
[likeButton release];


I hope it helps you all!


Known issues
  • Login Dialog has a double title bar, the FBDialog title and the FB webpage title. I don't think there is an easy solution for this, but I consider it as a minor issue.
  • FBCustomLoginDialog and FBLoginDialog uses different login methods, so the user have to make login twice if you use the FB iOS SDK for other staff. I don't think there is a workaround for this issue.
  • UI colors may change a little time after the button loads, as commented before, and it may stop working on the future if FB changes it's webpage structure.
  • There is no easy way for customizing layout or even alignment of the elements.
  • UIWebview have a little render delay as it loads remote content.

12 comentarios:

  1. Do you know how to make Like button work when we click like on them? I have already used the UIWebView to display like button, but when click on it, if not connected Facebook, One connect facebook dialog open, and we enter usernama and pass, but it can't connect facebook OK, it still stand at this untill i close dialog. And if application connected Facebook, one other connect facebook with empty content, we must close it to continue app. And no thing happen for like action. It's still working OK with post article to facebook. But can't do like. Can you know how to fix it?

    ResponderEliminar
  2. Have you used the above code? and, are you using a correct url? see that the init requires a url related to the thing you want to "like".
    If everything is OK, it should close the dialog just after the login success. Then, the notification is launch and all the existing UIWebViews with "like buttons" will refresh to draw the correct counter and profile.

    Take into account that this login is not the same that the Facebook API uses for the rest, so the user should login twice if he wants to do "like actions" and others like "wall posting".

    It is working for me... :( I need more info in order to help you...

    ResponderEliminar
  3. I used your code, but a little.
    I implement them in a FacebookController to do facebook action on my app with like action and post action.
    These are my code to reused from you in order to do like.

    -(UIWebView *) newLikeView:(NSString*)likeUrl width:(float) width x:(float)x y:(float)y
    {
    UIWebView * webView = [likeWebViews objectForKey:likeUrl];
    if(webView != nil) {
    [webView removeFromSuperview];
    [webView release];
    webView = nil;
    }
    webView = [[[UIWebView alloc] initWithFrame:CGRectMake(x, y, width, 25)] retain];
    NSString *likeFacebookUrl =[NSString stringWithFormat:@"http://www.facebook.com/plugins/like.php?layout=button_count&show_faces=true&action=like&colorscheme=light&href=%@", likeUrl ];
    NSString* escapedUrlString =[likeFacebookUrl stringByAddingPercentEscapesUsingEncoding:NSASCIIStringEncoding];
    NSURL *url = [NSURL URLWithString:escapedUrlString];
    NSURLRequest *request = [[NSURLRequest alloc] initWithURL:url];
    [webView loadRequest:request];
    webView.opaque = NO;
    webView.backgroundColor = [UIColor whiteColor];
    webView.delegate = self;
    [self.likeWebViews setValue:webView forKey:likeUrl];
    [[[webView subviews] lastObject] setScrollEnabled:NO];
    return webView;
    }

    - (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType {


    NSLog(@"URL de Facebook no contemplada: %@", [[request URL] absoluteString]);
    if (([[[request URL] absoluteString] rangeOfString:@"/connect/"].location!=NSNotFound) || ([[[request URL] absoluteString] rangeOfString:@"like.php"].location!=NSNotFound)){
    return YES;
    }
    // if user has to log in, open a new (modal) window
    if ([[[request URL] absoluteString] rangeOfString:@"login.php"].location!=NSNotFound){
    if (_loginDialog!=nil) {
    [_loginDialog release];
    _loginDialog = nil;
    }
    _loginDialog= [[[LikeFBLoginDialog alloc] init] autorelease];
    [_loginDialog loadURL:[[request URL] absoluteString] method:@"GET" get:nil post:nil];
    _loginDialog.delegate = self;
    [_loginDialog show];
    [_loginDialog.delegate retain]; //Retenemos el boton que ha abierto el login para que pueda recibir la confirmacion correctamente
    return NO;
    }

    return NO;
    }

    But it still be like i represent as my past post.

    ResponderEliminar
  4. Sorry for my two continuous posts because of the limit of characters to post on one. Can you help me solve this problem with below information?
    Thank you!

    ResponderEliminar
  5. trying to ge this to work with no success. what is frame suppose to equal

    ResponderEliminar
  6. hi i want to integrate Like button in my android application so please help in that front to add like button in my android application

    ResponderEliminar
  7. Hi Angel García Olloqui,

    do you have any work around for automatic Like action after Login, I mean i do not want user to press the like Button Twice, i don't know if it is bug but in my implementation after Login user has to click on it again.

    Thanks

    ResponderEliminar
  8. I have the same problem also, user has to press the like button twice. Please let us know if there's a solution for that issue.

    Thanks!

    ResponderEliminar
  9. i am using ur code and found folloe=wing console statement
    URL de Facebook no contemplada: http://www.facebook.com/dialog/optin?display=popup&app_id=127760087237610&secure=false&social_plugin=like&return_params=%7B%22layout%22%3A%22standard%22%2C%22show_faces%22%3A%22true%22%2C%22width%22%3A%2224%22%2C%22height%22%3A%2224%22%2C%22action%22%3A%22like%22%2C%22colorscheme%22%3A%22light%22%2C%22href%22%3A%22https%3A%2F%2Fwww.facebook.com%2Fpages%2FStupid-Bob%2F490152907679754%22%2C%22ret%22%3A%22optin%22%2C%22act%22%3A%22connect%22%7D&login_params=%7B%7D

    ResponderEliminar
  10. Have you solved this issue? I also meet this problem.

    ResponderEliminar