WebKit on the iPhone (Part 1)

If you develop an application which should display a web page or HTML file, you can use the WebKit framework, which is part of the MacOS and also of the iPhone OS.

But while on the Mac, the WebKit framework provides almost 160 public header files which define even more public classes and tons of methods you can call to get control of all aspects of loading, rendering, displaying and modifying a web page, on the iPhone there’s only one single class (UIWebView) with has just about a dozen methods you can use. Though internally the UIWebView class uses the same WebKit framework that is available on the Mac as public API, this API is private on the iPhone and therefore can’t be used. The small number of methods of the UIWebView class are sufficient to display nicely formatted text in help screens for example, but for a web browser (like iCab Mobile) or other web-based apps this isn’t enough, many essential features are missing.

Some examples:

  • UIWebView doesn’t provide a method to get the title of the currently displayed web page,
  • it just ignores all attempts to open links which are meant to open in new windows or tabs
  • it doesn’t allow accessing the HTML tree

WebKit itself provides many classes for all these tasks, but all of them are private and not available on the iPhone.

Some of the alternative browsers which are available in the AppStore just declare these limitations as feature (for example they advertise the inability to open new windows or Tabs as “no anoying popup window”). This sounds great, but of course this doesn’t make such browsers useful in the real world.

So what can we do to overcome these limitations of the UIWebView class? Can we (re)implement all the cool features of the WebKit framework which is available on the Mac on the iPhone as well without violating the  iPhone SDK agreements with Apple? Unfortunately, we can’t. But we can implement many of the missing feature.

If you look at the available methods, there’s only one, which would allow access to the content of the web page, and this is more or less the only way to get back  the missing features. And this method is

- (NSString *)stringByEvaluatingJavaScriptFromString:(NSString *)script;

This method is used to execute JavaScript code in the context of the current web page and get back a string as result. This means that we have to use JavaScript code to implement the features we need.

Let’s start with something that is very easy. We implement methods to get the title and the URL of the currently displayed web page. We implement this as an Objective C “Category”, so we don’t need to subclass UIWebView:

File: MyWebViewAdditions.h

@interface UIWebView (MyWebViewAdditions)
- (NSString*)title;
- (NSURL*)url;
@end

File: MyWebViewAdditions.m

#import "MyWebViewAdditions.h"

@implementation UIWebView (MyWebViewAdditions)

- (NSString*)title
{
    return [self stringByEvaluatingJavaScriptFromString:@"document.title"];
}

- (NSURL*)url
{
    NSString *urlString = [self stringByEvaluatingJavaScriptFromString:@"location.href"];
    if (urlString) {
        return [NSURL URLWithString:urlString];
    } else {
        return nil;
    }
}

@end

What are we doing here?
From the JavaScript’s point of view a web page is represented by the “document” object which has several properties. One of the properties is the “title” property which contains the title of the page. So with “document.title” we can access the title of the document within JavaScript. And this is exactly what we need to pass as a parameter to the method “stringByEvaluatingJavaScriptFromString:” to get the document title.

For retreiving the URL we do something similar.

So whenever we need to get the title or URL of the web page that is displayed in a UIWebView object, we only need to call the “title” or “url” method:

NSString *title = [anyUIWebViewObject title];

The next limitation we may want to address is the inability to open links which would open in a new window. The WebKit on the Mac would just call a delegate method of the host application to request that a new WebView object is created for an URL request. The application would then create a new WebView object and load the new page there. But on the iPhone the UIWebView doesn’t support such a delegate method and so all attempts to open such a link are just ignored.

These links do usually look like this:

<a href="destination" target="_blank">Link Text</a>

The “target” attribute defines where the link will open. The value can be a name of a frame (if the web page has frames), the name of a window or some reserved target names like “_blank” (opens a new window), “_self” (the window itself), “_parent” (the parent frame, if there are nested frames) and “_top” (the top-level or root frame, or identical to “_self” if the page doesn’t use frames).

As a first step, we want to tap on a such a link in our iPhone App, and the link should open like any other normal link in the same UIWebView object. What we need to do is simple: we need to find all links with a “target” attribute set to “_blank” and change its value to “_self“. Then the UIWebView object will no longer ignore these links. To be able to modify all of the link targets we have to wait until the page has finished loading and the whole web page content is available. Fortunately UIWebView provides the delegate method

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

which will be called when the web page has finished loading. So we have everything we need: We get notified when the page has loaded, and we know a way to access and modify the web page content (using “stringByEvaluatingJavaScriptFromString:“).

First we write our JavaScript code. Because this will be a little bit more code than what was needed to get the document title, it’s a good idea to create an extra file for our JavaScript code and then we add this file to the resources of our project in XCode:

File: ModifyLinkTargets.js:

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');
            }
        }
    }
}

What is this JavaScript function doing, when called?
It gets an array of all links (“a” tags) and then loops through all of these tags, checks if there’s a target attribute with the value “_blank“. If this is the case it changes the value to “_self“.

Note: There are other tags which can have a “target” attribute, like the “form” tag and the “area” tag. So you can use the “getElementsByTagName()” call to get these tags as well and modify their target attributes in the same way as I’ve done this for the “a” tag.

In our iPhone App we need to define a delegate for the UIWebView object and this delegate object will be called whenever the web page has finished loading. This is the method that is called in the delegate by the UIWebView object:

- (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()"];
}

What is this code doing?
At first the access path of the JavaScript file we created before is retreived from the application bundle and we load the content of the file (the JavaScript code) into a string. Then we execute/inject this code into the web page and finally we call out JavaScript function
which is modifying the link targets.

Some notes:

  • Getting the JavaScript file from the application bundle and loading it into a string should be usually done somewhere in the init methods of your UIWebView delegate object. This way the string with our JavaScript code is only loaded once and can be simply reused whenever a new link is clicked and a new page is loaded.
  • Using a long name for our JavaScript function which also includes a prefix like “MyIPhoneApp_” makes it unlikely that the code we inject into a web page will interfere or confict with functions and variables which the web page itself has already defined for its own purposes. This is especially important when we modify web pages we haven’t created ourselves and where we can not predict which function or variable names the JavaScript code of the web page is already using.
  • Using separate calls of “stringByEvaluatingJavaScriptFromString” to first injecting our own JavaScript code and then calling our own JavaScript function to start modifying the link targets seems to be more complicated that necessary. And for this simple example you would be right. But it is likely that you’ll define much more additional JavaScript functions for many different tasks as well. Some of the tasks are started when the page has finished loading (like modifying the link targets), but some tasks will be started later and maybe even multiple times. And so it makes much sense that injecting the code and calling the functions are done in separate calls.
  • The delegate method “webViewDidFinishLoad:(UIWebView *)webView” is called for each frame, not only when the page itself has finished loading. This means that this delegate method can be called multiple times while a single web page is loaded. I think that this can be called a bug in the iPhone OS, but nevertheless it is important to know. When you modify the web page, be aware that this might be done multiple times and so make sure that none of your modifications will have bad side effectes when being modified a second time.

What next?

  • The above example code does not cover web pages where new windows are opened using JavaScript.
  • The links will open in the same window, which is fine because they are no longer ignored. But they still don’t open in a new window or Tab.

More about this topic and the cases which are not yet covered will come in the second part of the “WebKit on the iPhone” article.

Feel free to ask questions and write comments. I’d like to get some feedback.