Skip to content


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.

Posted in iPhone & iPod Touch, Programming.

Tagged with , , , .


77 Responses

Stay in touch with the conversation, subscribe to the RSS feed for comments on this post.

  1. akp says

    Hi,

    I am loading the file in UIWebView and hiding the scroll
    I want to scroll the page programatically by number of lines of text. Any help how can I do that using java script.

  2. akp says

    I searched I have the same question but there was no answer for this question in blog?

    Hi,
    I am trying to scroll webview to an offset when a button click occurs [Page up and Page down buttons], for that i have to know the following things,

    i) Scrollable content height,
    ii) How to move the webview to an offset of the screen.

    If u have any info , would be appreciated

    http://www.iphonedevsdk.com/forum/iphone-sdk-development/30523-incorrect-javascript-scrolling-uiwebview.html

  3. Alexander Alexander says

    @akp
    There’s only one way to scroll a web view programmatically, and this is by using the JavaScript function “scrollTo”:

    window.scrollTo(x, y);

    The coordinates are document coordinates, not device coordinates.

  4. JC says

    Hello Alexander.

    Is there a simple way to make the UIWebView scroll more “smoothly” similar to Safari, where it offloads the offscreen portions with the “gray area”? Could this be done with CATileLayer?

  5. Alexander Alexander says

    @JC
    There seems to be no public API to get the smooth “checkerboard scrolling” from in Safari.
    In case you find a way to implement this using public APIs only, please let me know.

  6. Sam says

    Thank you for this blog. I’m learning a lot.
    I have a question about using Javascript like you do in some of your other posts. Can you detect when the web page changes due to Javascript? I’m trying to use your method for links with target=”_blank”, but it’s not working on the Google Reader site. Those links are created by javascript after the page has loaded and the user has clicked on an entry. Before that, they don’t have the target attribute. How do you detect links where the target attribute is added by javascript? Thank you.

  7. Alexander Alexander says

    @Sam
    You won’t get notified when the page changes due to JavaScript. The only chance would be to do some checks yourself. You could do this on touch events or also every x seconds or so.

  8. ashish says

    @Alexander
    I have the same question in this thread but its not answered properly. Any help from your point of view, how to deal with this?
    http://stackoverflow.com/questions/2707210/uiwebview-paging-line-cut-off

  9. Alexander Alexander says

    @ashish
    There’s no simple solution here. You would have to manually measure the line heights of the text to be able to correct the scrolling offset. But please note that it is possible that you can’t find any offset at all which would not cut off a line of text. This can happen when a web page uses multiple columns and the text of these columns is offset by a half line.

    Maybe a better pragmatic solution would be to always scroll less than a full page height. So the last line that is shown before scrolling (fully or only partially) will be shown as the first line after scrolling. This way you don’t need to be really exact.

  10. ashish says

    thanks Alexander, as you are saying that “always scroll less than a full page height”.
    while loading a file to webview, the line space and paragraph spaces we can not determine where they will occur. How would be possible to calculate page height?

  11. ashish says

    also forgot to ask how stanza and other readers are doing it? Are they not using webview?

  12. Nilesh says

    Great work man, Thank you very much….

  13. Marzzz says

    Hey you said that smooth checkerboarded scrolling is Not possible outside safari but it seems it’s no longer True as many browsers implement this now – could you Please explain me how this works eg why smooth scrolling is Not possible without the side effect of annoying checkerboarding??? Is the whole page converted to bitmap or smth like this so browser is cheating to get smoother scrolling? Checkerboards are highly annoying on IPad 1 (especially the ones that occur during page loading) but jerky scrolling on image intensive sites with browsers that doesnt use this technique is annoying as Well… Is IPad’s Cpu that weak it Cant scroll simple image gallery without choking? And one more thing which I don’t understand – In checkerboarding browsers while page is still loading and you scroll down to checkerboard it won’t dissapear until you stop scrolling and lift your finger – If you don’t checkerboard will remain visible… And it’s Not enough to lift your finger you have stop page from scrolling (“moving”) completly. Why is that? If browser has Scrollbar like icab’s scrollpad and you scroll page using it loading will continue regardless of scroling… 

  14. Alexander Alexander says

    @Marzzz
    If another browser shows a checkered pattern while scrolling, it uses a private (and therefore forbidden) API. Apple might remove the Apps if they find out.

  15. Marzzz says

    Hmm, I don’t understand – iCab mobile does this…

  16. Alexander Alexander says

    @Marzzz
    No, iCab doesn’t show a checkered pattern. Or what exactly do you mean?

  17. Bhavik says

    Hi, This post working best for _blank and _new but for onclick() it doesnt work.
    Any idea how to deal with this ??
    Thanks

  18. Alexander Alexander says

    @Bhavik
    The “onclick” handler is just an event handler. It is not directly related to links or new windows. So the real question is, what is the “onclick” hander doing exactly?

    BTW: this blog post is in parts outdated. In the latest releases of the iOS, links with a target “_blank” do work now without any additional code (the latest iOS releases will ignore the “_blank” automatically). But of course, if you want to open the links in a new tab/window, you have to use additional code and modify the original links.

  19. nikhil says

    Hello,
    I have a HTML content on webview and i wanted to search the string present on the webview.If the string is there i want to set the scroll to that position dynamically.Please give some solution over this problem.

    thanks

  20. Alexander Alexander says

    @nikhil
    You need to identify the HTML element which contains the text and then call the Javascript function scrollIntoViewIfNeeded(true) on this element. In one of my other posts in this blog, I’ve shown a way to search within the HTML content. This will automatically create SPAN elements around the found text in order to highlight. And this SPAM element is the one you can use to scroll into the view, wit the functioned mentioned above.

  21. Zee says

    Hi
    I am using the js for searching on UIWebview contents (used from your other post ). searching is fine but the scrolling is not working accordingly, not scrolling view to display highlighted text.
    I called this function to java script span element.
    span.scrollIntoViewIfNeeded(true);

    Any idea?
    Thanks

  22. Alexander Alexander says

    @Zee

    Calling span.scrollIntoViewIfNeeded(true); should work fine. Please make sure that the “span” variable is really a reference to the correct span element. Also please note that you need wait until the web site has finished loading before you can search and scroll.

  23. Eric says

    Good article
    I have met a trouble. I have a UIWebView, It can load a webpage first time,when i click on it ,i want open a new webView to load, in shouldStartLoadWithRequest i catch UIWebViewNavigationTypeLinkClicked navigationType to open a new webView, but some url open by click,just like ‘window.location.href=’,the navigtaion type is UIWebViewNavigationTypeother, when i do like this
    (navigationType == UIWebViewNavigationTypeLinkClicked || navigationType == UIWebViewNavigationTypeOther){
    openNewPage;
    }
    It always open in new page besides first time,But it does not what i want.Any suggestion?

  24. Alexander Alexander says

    @Eric
    “UIWebViewNavigationTypeLinkClicked” is only used when you tap on a normal link. If the web page opens a new page via “location.href” or “window.open()” or when loading a web page via “loadRequest” method of UIWebView, then “UIWebViewNavigationTypeOther” is used. So the “navigationType” is often much too unspecific to be really useful.

    So if you need to open link in a new window only once, you need to remember somewhere that you’ve opened a link already, so the next time you open that link, you
    can prevent that you open it in a new window again.

    The best way to do this depends on what exactly you need to do. You could simply store all the URLs you open in an array together with the information if these should or should not be opened in a new window again. You can also inject some JavaScript into the web page which modifies all links, so they will “change” once you’ve clicked on them the first time (for example by replacing the URL scheme from “http” to a custom one, so you can regingize links you’ve opened in the past by the different custom URL scheme).

  25. Eric says

    Maybe my description is not clear
    My mean that there is a UIWebView inside a UIView(a) and i just want use it one time. When i click the URL on it ,whatever the html open type ,I only want open it in a new view(b) which contained a new UIWebView. The other operation all on the new view(b),and have no relationship with the view(a). Any suggestion?

  26. Alexander Alexander says

    @Eric
    OK, if you just want to open any link from the first UIWebView in another WebView, then you should simply implement the UIWebView delegates “webViewDidStartLoad:”, “webViewDidFinishLoad:” and “webView:shouldStartLoadWithRequest:navigationType:”.

    When loading the initial page in the first UIWebView, you return YES for all requests in “webView:shouldStartLoadWithRequest:navigationType:”, so the initial page can load. After the page has loaded (which means “webViewDidFinishLoad:” was called as many times as “webViewDidStartLoad:”), you set a flag, and now when “webView:shouldStartLoadWithRequest:navigationType:” is called again, you return NO and instead create the new UIWebView and load the request there.

    There can be still some issues you may need to care about, if the initial web page dynamically loads new stuff automatically (like ads which are exchanged), because these might also end up in “webView:shouldStartLoadWithRequest:navigationType:” even though these are not links. But the iOS does not tell you this. So depending of the web page you need to work with, you may need to do some additional checks to make sure that you only react on the right links.

    One way to do this would be to inject JavaScript code into the initial web page which modifies all the links you want to control, so you can identify those links within the “webView:shouldStartLoadWithRequest:navigationType:” method.

1 2

Continuing the Discussion

  1. Saving an Image from UIWebView | Steili.com linked to this post on May 1, 2010

    [...] iCab Blog for the elementFromPoint idea – see Alexander’s comment on 9/1 11:55am, and the support [...]



Some HTML is OK

or, reply to this post via trackback.