September 24, 2012

Opening all URLs with Cordova’s ChildBrowser Plugin

cordova_bot

For anyone who has had the pleasure of working with Cordova you may hit this wall if you had to deal with external links. Tapping on a link opens the URL right in the WebView that your application is sitting in. The contents of the new page take over your application entirely and the user has no choice but to force close the app.

There are a couple of solutions to tackle this issue from a Cordova perspective.:

Note: These solutions are for Cordova 2.0.0. Earlier Cordova versions have minor differences, but these techniques can be applied.

Open all URLs externally in Mobile Safari

  1. In XCode, open the MainViewController.m class.
  2. Find the method: shouldStartLoadWithRequest, it should be commented out by default
    - (BOOL) webView:(UIWebView*)theWebView shouldStartLoadWithRequest:(NSURLRequest*)request navigationType:(UIWebViewNavigationType)navigationType
    {
        return [super webView:theWebView shouldStartLoadWithRequest:request navigationType:navigationType];
    }
    
  3. Implement the following:
    - (BOOL)webView:(UIWebView *)theWebView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType
    {
        NSURL *url = [request URL]; // URL that was requested
    
        // Test that URL scheme is either HTTP(S)
        if ([[url scheme] isEqualToString:@"http"] || [[url scheme] isEqualToString:@"https"]) {
            [[UIApplication sharedApplication] openURL:url]; // forward to application router
            return NO;
        }
        else {
            return [ super webView:theWebView shouldStartLoadWithRequest:request navigationType:navigationType ];
        }   
    }
    

The shouldStartLoadWithRequest method gets called before a WebView is about to load a URL. Here we intercept the request and pass it on to UIApplication. UIApplication serves as a global application router. Mobile Safari is registered to handle http hrefs (Mail.app to mailto, other applications to their custom protocols, etc..). So when we call the openURL method, Mobile Safari is invoked. We return NO (false) to the method to tell the WebView that we will not load the frame internally.

ChildBrowser Plugin

Let’s say you don’t want the user to leave your application, why would you? This is where Cordova’s ChildBrowser plugin comes into play. The ChildBrowser creates a WebView inside your main Cordova WebView. It has a bottom toolbar with necessary buttons (Done, Back, Forward, etc..).

Installing the ChildBrowser plugin is straightforward, you can follow these excellent tutorials:

To get it to do what we need, open ALL URLs through the ChildBrowser we must do the following:

  1. In XCode, open the MainViewController.m class.
  2. Find the method: shouldStartLoadWithRequest, it should be commented out by default
  3. Include the necessary ChildBrowser classes at the top of your Controller implementation class
    #import "MainViewController.h"
    #include "ChildBrowserViewController.h" // Include ChildBrowserViewController so we can use it later
    
    @implementation MainViewController
    
  4. Implement the shouldStartLoadWithRequest with the following:
    - (BOOL)webView:(UIWebView *)theWebView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType
    {
    
        NSURL *url = [request URL]; // URL that was requested
    
        // Test that URL scheme is either HTTP(S)
        if ([[url scheme] isEqualToString:@"http"] || [[url scheme] isEqualToString:@"https"]) {
            [theWebView sizeToFit];
            ChildBrowserViewController* childBrowser = [ [ ChildBrowserViewController alloc ] initWithScale:FALSE ];
            childBrowser.modalPresentationStyle = UIModalPresentationFormSheet;
            childBrowser.modalTransitionStyle = UIModalTransitionStyleFlipHorizontal;
            [super presentModalViewController:childBrowser animated:YES ];
            NSString* urlString=[NSString stringWithFormat:@"%@",[url absoluteString]];
            [childBrowser loadURL:urlString];
            [childBrowser release];
            return NO;
        }
        else {
            return [ super webView:theWebView shouldStartLoadWithRequest:request navigationType:navigationType ];
        }   
    }
    
  5. In your JavaScript, all that is necessary is to “install ChildBrowser” (ChildBrowser.install()). There is no need to invoke it directly to open URL. All URL requests are now intercepted.
    Here we take the same approach as the Safari method. However, instead of passing on to the UIApplication reference we invoke the ChildBrowser directly.

A note on iFrames and YouTube

Cordova has a nasty bug/feature/issue that it opens all iFrames as they are added to the DOM. Thus if you have a YouTube video in an iFrame that you append to the DOM, it will open the ChildBrowser right away and begin displaying the video. Unfortunately we do not know where the URL request came from in the MainViewController implementation, all we have is the URL. Thus we can make conditions based on the destination (for example YouTube or Vimeo) and handle it by default with Cordova.

In Cordova.plist you can set the following properties to allow inline media playback. This will allow your YouTube video to play inside your original content and not take over the entire WebView.

References

Modus Create is a product development, training, and services company based out of Reston VA. We help clients realize their product vision, and build in-house development capabilities.

Interested in working with us, or have a question? Feel free to contact us anytime, or call us directly at 1-855-663-8727.

  • http://www.facebook.com/mike994 Mike Gee

    Awesome article I’ve been searching for a way to do this for a while.. I am still on phonegap 1.2 because this issue was holding me back… Thanks!
    One question about YouTube; if I put a video in an iframe with online media set to yes, the video will play directly inside the app? Also, what about landscape orientation? Do I modify anything

    • http://www.facebook.com/mike994 Mike Gee

      *modify anything in the delegate file or does it automatically accept landscape views?

      • Stan Bershadskiy

        Yes, that is correct it should play in line.
        For landscape you would have to set a CSS class on that iframe and use a media query to define the width/height. YouTube will handle the scaling.

        • http://www.facebook.com/mike994 Mike Gee

          What would be the best way to go about it if your app was only in portrait mode?

          • Stan Bershadskiy

            You can just hard set a width and height of the iframe:
            node.getElementsByTagName(“iframe”)[0].width = “280″;
            I personally do that

  • http://www.facebook.com/mike994 Mike Gee

    Awesome article I’ve been searching for a way to do this for a while.. I am still on phonegap 1.2 because this issue was holding me back… Thanks!
    One question about YouTube; if I put a video in an iframe with online media set to yes, the video will play directly inside the app? Also, what about landscape orientation? Do I modify anything

    • http://www.facebook.com/mike994 Mike Gee

      *modify anything in the delegate file or does it automatically accept landscape views?

      • Stan Bershadskiy

        Yes, that is correct it should play in line.
        For landscape you would have to set a CSS class on that iframe and use a media query to define the width/height. YouTube will handle the scaling.

        • http://www.facebook.com/mike994 Mike Gee

          What would be the best way to go about it if your app was only in portrait mode?

          • Stan Bershadskiy

            You can just hard set a width and height of the iframe:
            node.getElementsByTagName(“iframe”)[0].width = “280″;
            I personally do that

  • Pingback: Announcing the Diablo 3 Mobile Companion | Modus Create

  • Michael SBCERA

    I implemented this, but am having some issues. I am using Cordova 1.8.1 and JqueryMobile. I also use the plugin TabBar and NavigationBar. The first issue is I have an iFrame on a page that opens as what looks like a dialog box when the app loads. I’m not clear on what your solution is for this. It is not a YouTube page, but simply a web page in an iFrame. Second issue is that my TabBar is now pushed down when I add your shouldStartLoadWithRequest code. The titles have been pushed down out of view.

  • Michael SBCERA

    I implemented this, but am having some issues. I am using Cordova 1.8.1 and JqueryMobile. I also use the plugin TabBar and NavigationBar. The first issue is I have an iFrame on a page that opens as what looks like a dialog box when the app loads. I’m not clear on what your solution is for this. It is not a YouTube page, but simply a web page in an iFrame. Second issue is that my TabBar is now pushed down when I add your shouldStartLoadWithRequest code. The titles have been pushed down out of view. (edit: I removed size to fit and this was fixed)

  • Michael SBCERA

    Hello?? Is there a solution for iFrame that opens on loading of app when this code is added?

  • Michael SBCERA

    Hello?? Is there a solution for iFrame that opens on loading of app when this code is added?

  • Michael SBCERA

    There is an additional bug with this code. If you use this code it will open your link in childbrowser portrait mode even if you are in landscape mode. This is latest CB as of 10/18/12 and iOS6

    You must comment out in ChildBrowserViewController.m:

    /*- (NSUInteger)supportedInterfaceOrientations
    {
    if ((self.orientationDelegate != nil) && [self.orientationDelegate respondsToSelector:@selector(supportedInterfaceOrientations)]) {
    return [self.orientationDelegate supportedInterfaceOrientations];
    }

    return UIInterfaceOrientationMaskPortrait;
    }*/

    • Tyler

      I have commented this section out an ChildBrowser still only launches in portrait mode. I have set my app to only support Landscape Left and Landscape Right. I have even tried changing the last line of the section above to be return UIInterfaceOrientationMaskLandscape; to no avail. Any suggestions?

      • Michael SBCERA

        Tyler did you figure this out? Sorry I wasn’t on discus or whatever this is. I can have the email alerts sent to me so let me know.

  • Michael SBCERA

    There is an additional bug with this code. If you use this code it will open your link in childbrowser portrait mode even if you are in landscape mode. This is latest CB as of 10/18/12 and iOS6

    You must comment out in ChildBrowserViewController:

    /*- (BOOL)shouldAutorotate
    {
    if ((self.orientationDelegate != nil) && [self.orientationDelegate respondsToSelector:@selector(shouldAutorotate)]) {
    return [self.orientationDelegate shouldAutorotate];
    }

    return YES;
    

    }

    • (NSUInteger)supportedInterfaceOrientations
      {
      if ((self.orientationDelegate != nil) && [self.orientationDelegate respondsToSelector:@selector(supportedInterfaceOrientations)]) {
      return [self.orientationDelegate supportedInterfaceOrientations];
      }

      return UIInterfaceOrientationMaskPortrait;
      }

    • (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation
      {
      if ((self.orientationDelegate != nil) && [self.orientationDelegate respondsToSelector:@selector(shouldAutorotateToInterfaceOrientation:)]) {
      return [self.orientationDelegate shouldAutorotateToInterfaceOrientation:interfaceOrientation];
      }

      return YES;
      }*/

    • Tyler

      I have commented this section out an ChildBrowser still only launches in portrait mode. I have set my app to only support Landscape Left and Landscape Right. I have even tried changing the last line of the section above to be return UIInterfaceOrientationMaskLandscape; to no avail. Any suggestions?

  • dmackerman

    Here’s a comment.

  • dmackerman

    Here’s a comment.

  • http://twitter.com/BuzzstarApps Buzzstar Apps

    Working like charm :* thanx alot m using phonegap 2.2.0 and xcode 4.5

  • http://twitter.com/BuzzstarApps Buzzstar Apps

    Working like charm :* thanx alot m using phonegap 2.2.0 and xcode 4.5

  • Luke Mallon

    You could use this JavaScript. (jQuery used for click event)

    $(‘a.social’).click(function (event) {
    if (typeof navigator.app !== ‘undefined’) {
    event.preventDefault();
    navigator.app.loadUrl($(this).attr(‘href’), { openExternal : true }); // <– does the required work
    }
    });

    This will load the url in the appropriate application if it is say youtube or facebook or google+ etc.. I have tested this on version 2.0.0 only.

  • Luke Mallon

    You could use this JavaScript. (jQuery used for click event)

    $(‘a.social’).click(function (event) {
    if (typeof navigator.app !== ‘undefined’) {
    event.preventDefault();
    navigator.app.loadUrl($(this).attr(‘href’), { openExternal : true }); // <– does the required work
    }
    });

    This will load the url in the appropriate application if it is say youtube or facebook or google+ etc.. I have tested this on version 2.0.0 only.

  • http://twitter.com/MeganSime Megan Sime

    is there a way to change this so that i can view downloaded files in childBrowser?? i can hardly find any information helping me with this.