WebKit on the iPhone (Part 2)

In the first part of this tutorial you’ve learned that you can use JavaScript code to get information about the web page and to modify links which would be normally ignored by UIWebView (because they are supposed to open a new window) so that they will work just like any other link.

In the second part of the tutorial you’ll learn how to open links (which are supposed to open in a new window) in a new tab. On the iPhone you can’t open multiple windows, so we have to use “tabs” instead of windows. I’ll also show how to deal with opening windows or tabs via JavaScript when no HTML link is involved.

In part one we’ve written the JavaScript function “MyIPhoneApp_ModifyLinkTargets()” which loops through all links and replaces the link target “_blank” with “_self”. This way the links will open in the same tab and are no longer ignored by UIWebView. But if we really want to open these links in new tabs, we have to open a new  tab ourselves and then open the link there. This can’t be directly done within the JavaScript code. So we have to find a way to pass the information about the link URL and the link target to the Objective-C part of our app. The easiest way to do this to replace the original link URL by a new URL which includes all these information. In order to make it easy to recognize our modified URLs, we are creating the new URLs with a new custom URL scheme “newtab”. We just add this to the existing JavaScript function we’ve written in the last part of the tutorial.

function MyIPhoneApp_ModifyLinkTargets() {
    var allLinks = document.getElementsByTagName('a');
    if (allLinks) {
        var i;
        for (i=0; i<allLinks.length; i++) {
            var link = allLinks[i];
            var target = link.getAttribute('target');
            if (target && target == '_blank') {
                link.setAttribute('target','_self');
                link.href = 'newtab:'+escape(link.href);
            }
        }
    }
}

The additional line

link.href = 'newtab:'+escape(link.href);

will take the original URL and escapes all the characters with a special meaning (like “:” and “/”) and puts the new custom URL scheme “newtab” in front of it. The link URL “http://www.apple.com/” will be converted to “newtab:http%3A//www.apple.com/” for example.

Within the Objective-C code of our app, we have to implement the UIWebView delegate method

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

This delegate method is called whenever a link is tapped by the user. And the app can check the URL and return YES or NO, wether it is OK to open this URL or not. What we need to do is to check if the URL is one of the URLs which were modified by us.  If this is the case we create a new tab with a new UIWebView object and then open the original URL in this new UIWebView object. Finally we return NO as the result of the delegate method to indicate that this modified URL should not be opened. This is important because the page is already loading in the new tab. If we find out that this delegate method is called with an URL that was not modified, we just return YES, so the URL will open as expected. We know that the URL was modified, if the URL uses the URL scheme “newtab”. So the delegate method would look like this:

- (BOOL)webView:(UIWebView *)view shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType
{
    NSURL *url = [request URL];
    if ([[url scheme] isEqualToString:@"newtab"]) {
        NSString *urlString = [[url resourceSpecifier] stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
        url = [NSURL URLWithString:urlString relativeToURL:[view url]];
        [self openNewTabWithURL:url];
        return NO;
    }
    return YES;
}

The “resourceSpecifier” method of the NSURL object is returning everything but the URL scheme, which is exactly what we need to get the original URL. And because within the JavaScript code we’ve escaped the original URL we need to unescape it. And this can be done with “stringByReplacingPercentEscapesUsingEncoding:“. The method “openNewTabWithURL:” is not shown here, but it is obvious what it is supposed to do: it creates a new tab with a new UIWebView object and opens the URL in the new tab.

In the above code you’ll notice this line:

url = [NSURL URLWithString:urlString relativeToURL:[view url]];

We call “[view url]” here (calling the method we’ve written in the first part of the tutorial) to get the base URL, in case we’ve received only a relative URL from the JavaScript code. For normal links this wouldn’t be necessary because the “href” property of a link element will always return the absolute URL. But when dealing with windows or tabs which are opened using JavaScript code and not from within links, it is possible to receive only relative URLs, and therefore we need to be able to create valid absolute URLs.

And now it’s time to explain how new windows can be opened with JavaScript code, without any HTML links. Many web pages are doing this, often for popup window with advertising banners, but also sometimes for important stuff. So we might be able to deal with this as well.

In JavaScript there’s the call

window.open(url,target,parameters)

Where “url” is the URL to open, “target” is the target, similar to the target attribute of HTML links and “parameters” are paramters which tell the browser the location and size of the new window and if the new window should open with or without toolbars, etc. When calling “window.open()” on the iPhone with a target that is a new window, nothing will happen. This is because the UIWebView object doesn’t support new windows or tabs. So we need to overwrite the the original “window.open()” function of JavaScript so our own code is executed whenever “window.open()” is called. Our own code will then check if the target will open a new window. If this is the case, we create a new URL with the scheme “newtab”, like we’ve done this with the links. And then we also need to explicitly open the new URL, which can be done by assigning the URL to the JavaScript property “location.href“.

function MyIPhoneApp_ModifyWindowOpen() {
    window.open =
            function(url,target,param) {
                if (url && url.length > 0) {
                    if (!target) target = "_blank";
                    if (target == '_blank') {
                        location.href = 'newtab:'+escape(url);
                    } else {
                        location.href = url;
                    }
                }
            }
}

Please note that this code makes certain assumptions: There are no HTML frames used in the web page and the only target names are “_blank”, “_self”, “_top”, “_parent”. Dealing with frames requires some extra work and would make things more complicated. So this should be handled in a later tutorial.

In the delegate method “webViewDidFinishLoad:” we just need to call the JavaScript function “MyIPhoneApp_ModifyWindowOpen()” as well. So we add a new line of code:

- (void)webViewDidFinishLoad:(UIWebView *)webView
{
    NSString *path = [[NSBundle mainBundle] pathForResource:@"ModifyLinkTargets" ofType:@"js"];
    NSString *jsCode = [NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:nil];

    [webView stringByEvaluatingJavaScriptFromString:jsCode];

    [webView stringByEvaluatingJavaScriptFromString:@"MyIPhoneApp_ModifyLinkTargets()"];
    [webView stringByEvaluatingJavaScriptFromString:@"MyIPhoneApp_ModifyWindowOpen()"];
}

Now, whenever a web page uses the JavaScript call “window.open()“, to open a page in a new window, the delegate method “webView:shouldStartLoadWithRequest:navigationType:” is called, just like it is called when a link is opened. And because we’ve created a new URL with a “newtab” URL scheme when necessary, this is automtatically detected by the code we’ve written before.

What you’ve learned so far? You can write an iPhone app where a web page loaded in an UIWebView object is able to open new tabs, regardless if this is done using HTML links or via JavaScript using “window.open()”. The whole solution is more complicated than on the Mac where the WebView object is able to notify us about new windows or tabs which have to be created. But even if we have to use a mixture of Objective-C and JavaScript code to solve this task, it’s not too difficult.

Final notes:

There are still some details which are not covered by this tutorial. For example web pages can have “frames” and the targets referenced by links and “window.open()” can also address these frames. So you would need some additional code to check if a frame with the target name exists. If there’s a frame with the target name, then do not modify the link, if no frame exists, you should modify the link (using the “newtab” scheme) and treat the target name just like “_blank”.

Also “window.open()” will usually return a reference to the newly created window object so that the JavaScript code can access it later again. This can not be done on the iPhone using the iPhone SDK (at least not without violating the iPhone SDK agreement). But fortunately, most web pages don’t need to access the new windows later, so this limitation is usually not a big deal.