<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>iCab Blog &#187; WebKit</title>
	<atom:link href="http://www.icab.de/blog/tag/webkit/feed/" rel="self" type="application/rss+xml" />
	<link>http://www.icab.de/blog</link>
	<description>iCab related stuff; Mac, iPhone and Cocoa programming</description>
	<lastBuildDate>Sat, 21 Aug 2010 12:14:09 +0000</lastBuildDate>
	<language>en</language>
	<sy:updatePeriod>hourly</sy:updatePeriod>
	<sy:updateFrequency>1</sy:updateFrequency>
	<generator>http://wordpress.org/?v=3.0.1</generator>
		<item>
		<title>Customize the contextual menu of UIWebView</title>
		<link>http://www.icab.de/blog/2010/07/11/customize-the-contextual-menu-of-uiwebview/</link>
		<comments>http://www.icab.de/blog/2010/07/11/customize-the-contextual-menu-of-uiwebview/#comments</comments>
		<pubDate>Sun, 11 Jul 2010 17:15:26 +0000</pubDate>
		<dc:creator>Alexander</dc:creator>
				<category><![CDATA[Programming]]></category>
		<category><![CDATA[iPhone & iPod Touch]]></category>
		<category><![CDATA[Cocoa]]></category>
		<category><![CDATA[Development]]></category>
		<category><![CDATA[iPhone]]></category>
		<category><![CDATA[JavaScript]]></category>
		<category><![CDATA[WebKit]]></category>

		<guid isPermaLink="false">http://www.icab.de/blog/?p=159</guid>
		<description><![CDATA[When you tap and hold your finger on a link in a UIWebView object for about a second, a contextual menu will open and provides a few choices: copy the URL to the pasteboard, open the link and a button to close the menu again. Unfortunately there&#8217;s no API available to add additional menu items [...]]]></description>
			<content:encoded><![CDATA[<p>When you tap and hold your finger on a link in a UIWebView object for about a second, a contextual menu will open and provides a few choices: copy the URL to the pasteboard, open the link and a button to close the menu again. Unfortunately there&#8217;s no API available to add additional menu items to this contextual menu. But if you look at iCab Mobile, you&#8217;ll notice that there are a lot of additional menu items here. How is this done?</p>
<p>First of all, you really can&#8217;t add additional menu items to the default ones of the standard contextual menu. But you can switch off the contextual menu using a certain CSS property. So the solution would be to switch off the default menu and implement your own from scratch. And to implement your own contextual menu, you have to first catch the tab-and-hold gesture, get the coordinates of the finger on the screen, translate these coordinates into the coordinate system of the web page and finally look for the HTML elements at this location. Based on the HTML elements, you can then decide which menu items to include in the contextual menu, and then create and display the contextual menu.</p>
<p>The first step is to switch off the default contextual menu of UIWebView. This is easy because we only need to set the CSS property &#8220;-webkit-touch-callout&#8221; to &#8220;none&#8221; for the body element of the web page. We can do this using JavaScript in the UIWebView delegate method &#8220;webViewDidFinishLoad:&#8221;&#8230;</p>
<pre style="padding-left: 0.7em; border-left: 1px solid #030; color: #003300; font-size: 0.9em;">- (void)webViewDidFinishLoad:(UIWebView *)webView
{
   [webView stringByEvaluatingJavaScriptFromString:@"document.body.style.webkitTouchCallout='none';"];
}</pre>
<p>Now, the default Contextual menu won&#8217;t open anymore.</p>
<p>The next step is to catch the &#8220;tap-and-hold&#8221; gesture. If your App only needs to run on iOS 3.2, iOS 4.0 or newer, you can simply use the new UIGestureRecognizer API. But if you still want to address the 1st generation iPod Touch and iPhone devices (where you can install at most iOS 3.1) where these UIGestureRecognizer classes are not available, you need to do a little bit more. I&#8217;ll show here a solution which will work with iOS 3.0 as well, so it&#8217;s compatible to all iPhone, iPad and iPod Touch devices.</p>
<p>A big problem when you need to catch gestures within UIWebView is that UIWebView internally consists of many nested views, and only the inner ones do respond to events and gestures. Which means, if you subclass UIWebView and overwrite the methods &#8220;touchesBegan:withEvent:&#8221;, &#8220;touchesMoved:withEvent:&#8221; etc., you&#8217;ll notice that these methods won&#8217;t be called. The events are delivered directly to these private inner views which are actually doing all the work. And if you overwrite &#8220;hitTest:withEvent:&#8221;, which is used by the iOS to find the view to which the events will be delivered, you would be able to get the touch events, but then you would have to deliver the events to these inner views yourself so these can still do their work. And because these inner views are private and the relationships between these views are unknown, it can be extremely dangerous to mess with the events here. </p>
<p>A much easier way would be to subclass UIWindow and overwrite the method &#8220;sendEvent:&#8221; of the UIWindows class. Here you&#8217;ll get all events before they are delivered to the views. And we can deliver the tap-and-hold events using the notification manager to the rest of the app. Each object that is interested in this gesture can listen to this notification.</p>
<p>Recognizing the tap-and-hold gesture is not very complicated. What we need to do is to save the screen coordinates of the finger when the finger first touches the screen. At that time we will also start a timer which fires after about a second. As soon as another finger touches the screen, the finger moves or the touch event is canceled, we invalidate the timer because then it can not be a simple tap-and-hold gesture anymore. If the timer fires, we can be sure that a single finger has touched the screen for about a second without moving and then we&#8217;ve recognized the &#8220;tap-and-hold&#8221; gesture. We post the gesture as notification.</p>
<p>This is the implementation of the UIWindow subclass:</p>
<p><span style="color: #993300;">MyWindow.h:</span></p>
<pre style="padding-left: 0.7em; border-left: 1px solid #030; color: #003300; font-size: 0.9em;">
@interface MyWindow : UIWindow
{
   CGPoint    tapLocation;
   NSTimer    *contextualMenuTimer;
}
@end</pre>
<p><span style="color: #993300;">MyWindow.m:</span></p>
<pre style="padding-left: 0.7em; border-left: 1px solid #030; color: #003300; font-size: 0.9em;">#import "MyWindow.h"

@implementation MyWindow

- (void)tapAndHoldAction:(NSTimer*)timer
{
   contextualMenuTimer = nil;
   NSDictionary *coord = [NSDictionary dictionaryWithObjectsAndKeys:
             [NSNumber numberWithFloat:tapLocation.x],@"x",
             [NSNumber numberWithFloat:tapLocation.y],@"y",nil];
   [[NSNotificationCenter defaultCenter] postNotificationName:@"TapAndHoldNotification" object:coord];
}

- (void)sendEvent:(UIEvent *)event
{
   NSSet *touches = [event touchesForWindow:self];
   [touches retain];

   [super sendEvent:event];    // Call super to make sure the event is processed as usual

   if ([touches count] == 1) { // We're only interested in one-finger events
      UITouch *touch = [touches anyObject];

      switch ([touch phase]) {
         case UITouchPhaseBegan:  // A finger touched the screen
            tapLocation = [touch locationInView:self];
            [contextualMenuTimer invalidate];
            contextualMenuTimer = [NSTimer scheduledTimerWithTimeInterval:0.8
                        target:self selector:@selector(tapAndHoldAction:)
                        userInfo:nil repeats:NO];
            break;

         case UITouchPhaseEnded:
         case UITouchPhaseMoved:
         case UITouchPhaseCancelled:
            [contextualMenuTimer invalidate];
            contextualMenuTimer = nil;
            break;
      }
   } else {                    // Multiple fingers are touching the screen
      [contextualMenuTimer invalidate];
      contextualMenuTimer = nil;
   }
   [touches release];
}
@end</pre>
<p>Some remarks for the UITouchPhaseMoved phase: it can be sometimes useful to allow small movements on the screen. You can add some code to check for the distance the finger has moved and if it is within a certain range, you just don&#8217;t abort the timer. This helps users which have difficulties to hold the finger still for about a second.</p>
<p>Another important thing you have to do when the App window is created by a NIB file: you have to change the UIWindow class of the window within the NIB file in Interface Builder to the new subclass MyWindow. This way the window is created with our subclass, which is important.</p>
<p>The next step is to listen for the &#8220;TapAndHoldNotification&#8221; notification  within the UIWebView delegate and when this notification is received, we need to check which HTML element was touched.</p>
<p>When initializing the UIWebView delegate, we need to add the delegate as observer for the notification&#8230;</p>
<pre style="padding-left: 0.7em; border-left: 1px solid #030; color: #003300; font-size: 0.9em;">   [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(contextualMenuAction:) name:@"TapAndHoldNotification" object:nil];
}</pre>
<p>And here&#8217;s the method &#8220;contextualMenuAction:&#8221;&#8230;</p>
<pre style="padding-left: 0.7em; border-left: 1px solid #030; color: #003300; font-size: 0.9em;">- (void)contextualMenuAction:(NSNotification*)notification
{
   CGPoint pt;
   NSDictionary *coord = [notification object];
   pt.x = [[coord objectForKey:@"x"] floatValue];
   pt.y = [[coord objectForKey:@"y"] floatValue];

   // convert point from window to view coordinate system
   pt = [webView convertPoint:pt fromView:nil];

   // convert point from view to HTML coordinate system
   CGPoint offset  = [webView scrollOffset];
   CGSize viewSize = [webView frame].size;
   CGSize windowSize = [webView windowSize];

   CGFloat f = windowSize.width / viewSize.width;
   pt.x = pt.x * f + offset.x;
   pt.y = pt.y * f + offset.y;

   [self openContextualMenuAt:pt];
}</pre>
<p>The method &#8220;scrollOffset&#8221; and &#8220;windowSize&#8221; are implemented as category for the UIWebView class. &#8220;scrollOffset&#8221; is required to make sure the coordinates are also correct when the web page was scrolled. The &#8220;windowSize&#8221; returns the visible width and height of the HTML document from the point of view of the HTML document. So based on the windowsSize of the &#8220;HTML window&#8221; and the view size of the UIWebView, you can calculate the zoom factor, and the zoom factor is necessary to transform and scale the screen coordinates to the correct HTML coordinates.</p>
<p>Here&#8217;s the implementation of &#8220;scrollOffset&#8221; and &#8220;windowSize&#8221;&#8230;</p>
<p><span style="color: #993300;">WebViewAdditions.h:</span></p>
<pre style="padding-left: 0.7em; border-left: 1px solid #030; color: #003300; font-size: 0.9em;">@interface UIWebView(WebViewAdditions)
- (CGSize)windowSize;
- (CGPoint)scrollOffset;
@end</pre>
<p><span style="color: #993300;">WebViewAdditions.m:</span></p>
<pre style="padding-left: 0.7em; border-left: 1px solid #030; color: #003300; font-size: 0.9em;">#import "WebViewAdditions.h"

@implementation UIWebView(WebViewAdditions)

- (CGSize)windowSize
{
   CGSize size;
   size.width = [[self stringByEvaluatingJavaScriptFromString:@"window.innerWidth"] integerValue];
   size.height = [[self stringByEvaluatingJavaScriptFromString:@"window.innerHeight"] integerValue];
   return size;
}

- (CGPoint)scrollOffset
{
   CGPoint pt;
   pt.x = [[self stringByEvaluatingJavaScriptFromString:@"window.pageXOffset"] integerValue];
   pt.y = [[self stringByEvaluatingJavaScriptFromString:@"window.pageYOffset"] integerValue];
   return pt;
}
@end</pre>
<p>Finally, we need to implement the method &#8220;openContextualMenuAt:&#8221; for the UIWebView delegate, which first checks for the HTML elements that are at the touch locations and the creates the contextual menu. Checking for the HTML elements at the touch location must be done via JavaScript&#8230;</p>
<p><span style="color: #993300;">JSTools.js</span></p>
<pre style="padding-left: 0.7em; border-left: 1px solid #030; color: #003300; font-size: 0.9em;">function MyAppGetHTMLElementsAtPoint(x,y) {
   var tags = ",";
   var e = document.elementFromPoint(x,y);
   while (e) {
      if (e.tagName) {
         tags += e.tagName + ',';
      }
      e = e.parentNode;
   }
   return tags;
}</pre>
<p>This JavaScript function simply collects the tag names of all HTML elements at the touch coordinates and returns the tag names as string list. The JavaScript file must be added as &#8220;resource&#8221; to your XCode project. It can happen that Xcode treats JavaScript file as normal code and tries to compile and link it instead of adding it to the resources. So make sure the JavaScript file is in the &#8220;Copy Bundle Resources&#8221; section within the XCode project target and not in the &#8220;Compile Sources&#8221; or &#8220;Link Binaries&#8221; section.</p>
<pre style="padding-left: 0.7em; border-left: 1px solid #030; color: #003300; font-size: 0.9em;">- (void)openContextualMenuAt:(CGPoint)pt
{
   // Load the JavaScript code from the Resources and inject it into the web page
   NSString *path = [[NSBundle mainBundle] pathForResource:@"JSTools" ofType:@"js"];
   NSString *jsCode = [NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:nil];
   [webView stringByEvaluatingJavaScriptFromString: jsCode];

   // get the Tags at the touch location
   NSString *tags = [webView stringByEvaluatingJavaScriptFromString:
            [NSString stringWithFormat:@"MyAppGetHTMLElementsAtPoint(%i,%i);",(NSInteger)pt.x,(NSInteger)pt.y]];

   // create the UIActionSheet and populate it with buttons related to the tags
   UIActionSheet *sheet = [[UIActionSheet alloc] initWithTitle:@"Contextual Menu"
                  delegate:self cancelButtonTitle:@"Cancel"
                  destructiveButtonTitle:nil otherButtonTitles:nil];

   // If a link was touched, add link-related buttons
   if ([tags rangeOfString:@",A,"].location != NSNotFound) {
      [sheet addButtonWithTitle:@"Open Link"];
      [sheet addButtonWithTitle:@"Open Link in Tab"];
      [sheet addButtonWithTitle:@"Download Link"];
   }
   // If an image was touched, add image-related buttons
   if ([tags rangeOfString:@",IMG,"].location != NSNotFound) {
      [sheet addButtonWithTitle:@"Save Picture"];
   }
   // Add buttons which should be always available
   [sheet addButtonWithTitle:@"Save Page as Bookmark"];
   [sheet addButtonWithTitle:@"Open Page in Safari"];

   [sheet showInView:webView];
   [sheet release];
}</pre>
<p>This method injects the JavaScript code which looks for the HTML elements at the touch location into the web page and calls this function. The return value will be a string with a comma-separated list of tag names. This string will start and end with a comma so we can simply check for occurrences of a substring &#8220;,tagName,&#8221; if we want to find out if an element with a certain tag name was touched. In our example, we simple add some buttons if an &#8220;A&#8221; tag was hit and some other buttons if and &#8220;IMG&#8221; tag was hit. But what you&#8217;re doing is up to you. Also the information that is returned from the JavaScript function (in this example &#8220;MyAppGetHTMLElementsAtPoint()&#8221;) is up to you. In iCab Mobile it returns the HREF and SRC attributes of A and IMG tags, so the URLS can be directly processed.</p>
<p>The example doesn&#8217;t include the UIActionSheet delegate method which is called when you tab on one of the buttons in the contextual menu. But I think you should already know how to handle this. Also a few other details might be missing, but I think you should be able now to implement your own custom contextual menu for UIWebView objects with the information from the blog post.</p>
]]></content:encoded>
			<wfw:commentRss>http://www.icab.de/blog/2010/07/11/customize-the-contextual-menu-of-uiwebview/feed/</wfw:commentRss>
		<slash:comments>15</slash:comments>
		</item>
		<item>
		<title>Changing the headers for UIWebKit HTTP requests</title>
		<link>http://www.icab.de/blog/2010/04/07/changing-the-headers-for-uiwebkit-http-requests/</link>
		<comments>http://www.icab.de/blog/2010/04/07/changing-the-headers-for-uiwebkit-http-requests/#comments</comments>
		<pubDate>Wed, 07 Apr 2010 15:51:58 +0000</pubDate>
		<dc:creator>Alexander</dc:creator>
				<category><![CDATA[Programming]]></category>
		<category><![CDATA[iCab]]></category>
		<category><![CDATA[iPhone & iPod Touch]]></category>
		<category><![CDATA[Cocoa]]></category>
		<category><![CDATA[Development]]></category>
		<category><![CDATA[iCab Mobile]]></category>
		<category><![CDATA[iPhone]]></category>
		<category><![CDATA[WebKit]]></category>

		<guid isPermaLink="false">http://www.icab.de/blog/?p=144</guid>
		<description><![CDATA[I was asked several times, in which way the &#8220;User-Agent&#8221; header can be modified for the HTTP requests that are initiated from within the UIWebView object. iCabMobile is doing this, and also some other iPhone Apps, but the UIWebView API doesn&#8217;t provide anything which allows to modify the &#8220;User-Agent&#8221; information or any other HTTP header. [...]]]></description>
			<content:encoded><![CDATA[<p>I was asked several times, in which way the &#8220;User-Agent&#8221; header can be modified for the HTTP requests that are initiated from within the UIWebView object. iCabMobile is doing this, and also some other iPhone Apps, but the UIWebView API doesn&#8217;t provide anything which allows to modify the &#8220;User-Agent&#8221; information or any other HTTP header.</p>
<p>When you load a web page from the internet through UIWebView, you can provide a delegate which is called for each web page that is loaded. And in the method &#8220;webView:shouldStartLoadWithRequest:navigationType:&#8221; of the delegate, you&#8217;ll even get an NSURLRequest object you can look at, but unfortunately you can not modify this object. So there&#8217;s no way to change the default &#8220;User-Agent&#8221; information that is sent to the server, nor can you modify any other data.</p>
<p>When you&#8217;re loading data from the internet outside of UIWebView, you would probably use the NSURLConnection class. In this case you would create an NSURLRequest object (or the mutable counterpart NSMutableURLRequest) with all the HTTP headers for the request yourself (using the method &#8220;setValue:forHTTPHeaderField:&#8221;). You have full control over all of the HTTP headers you want to send to the server, including the &#8220;User-Agent&#8221; information.</p>
<p>When we assume that the UIWebView object will internally also use NSURLRequest or NSMutableURLRequest to create a HTTP request before this request is passed to the networking classes like NSURLConnection, we need a way to subclass or overwrite the method &#8220;setValue:forHTTPHeaderField:&#8221; of the NSMutableURLRequest class. Then we would be able to check for each HTTP header that is set for a NSMutableURLRequest, if this is the &#8220;User-Agent&#8221; header and if it is, we can modify it.</p>
<p>The only problem is that we can&#8217;t overwrite or subclass the NSMutableURLRequest class and force UIWebView to use our subclass instead of the original class. But iPhone Apps are written in Objective C and this programming language does allow exchanging and modifying classes, methods, variables etc. at runtime any time. So we can tell the Objective C runtime system that each time the method &#8220;setValue:forHTTPHeaderField:&#8221; of the &#8220;NSMutableURLRequest&#8221; class is called, our own method is called instead. This way it doesn&#8217;t matter that UIWebView will never call our method directly. Exchanging methods is called &#8220;Method Swizzling&#8221; and you can learn more about it on the <a href="http://www.cocoadev.com/index.pl?MethodSwizzling">CocoaDev page</a>.</p>
<p><b>The method swizzling is very powerful, but it can be also very dangerous if you don&#8217;t know what you&#8217;re doing. So be very careful.</b></p>
<p>Now to the sources. I&#8217;ve implemented the method swizzling as a category of NSObject, so you can use it for all classes very easy (but as I said above, be careful, don&#8217;t use it if there are other options).</p>
<p><span style="color: #993300;">MethodSwizzling.h:</span></p>
<pre style="padding-left: 0.7em; border-left: 1px solid #030; color: #003300; font-size: 0.9em;">@interface NSObject (Swizzle)

+ (BOOL)swizzleMethod:(SEL)origSelector withMethod:(SEL)newSelector;

@end</pre>
<p><span style="color: #993300;">MethodSwizzling.m:</span></p>
<pre style="padding-left: 0.7em; border-left: 1px solid #030; color: #003300; font-size: 0.9em;">#import "MethodSwizzling.h"

@implementation NSObject (Swizzle)

+ (BOOL)swizzleMethod:(SEL)origSelector withMethod:(SEL)newSelector
{
    Method origMethod = class_getInstanceMethod(self, origSelector);
    Method newMethod = class_getInstanceMethod(self, newSelector);

    if (origMethod &amp;&amp; newMethod) {
        if (class_addMethod(self, origSelector, method_getImplementation(newMethod), method_getTypeEncoding(newMethod))) {
            class_replaceMethod(self, newSelector, method_getImplementation(origMethod), method_getTypeEncoding(origMethod));
        } else {
            method_exchangeImplementations(origMethod, newMethod);
        }
        return YES;
    }
    return NO;
}

@end</pre>
<p>You can call &#8220;swizzleMethod:&#8221; for an object, passing in the selectors of the original and the new replacement methods. If the &#8220;swizzleMethod:&#8221; method returns with the result YES, each call of the original method will then call the replacement method and each call of the replacement method will call the original method. So within your replacement method you can still call the original method.</p>
<p>Here&#8217;s the implementation of the new replacement method for the NSMutableURLRequest class:</p>
<p><span style="color: #993300;">MyMutableURLRequest.h:</span></p>
<pre style="padding-left: 0.7em; border-left: 1px solid #030; color: #003300; font-size: 0.9em;">@interface NSMutableURLRequest (MyMutableURLRequest)

+ (void)setupUserAgentOverwrite;

@end</pre>
<p><span style="color: #993300;">MyMutableURLRequest.m:</span></p>
<pre style="padding-left: 0.7em; border-left: 1px solid #030; color: #003300; font-size: 0.9em;">#import "MyMutableURLRequest.h"
#import "MethodSwizzling.h"

@implementation NSMutableURLRequest (MyMutableURLRequest)

- (void)newSetValue:(NSString *)value forHTTPHeaderField:(NSString *)field;
{
    if ([field isEqualToString:@"User-Agent"]) {
        value = @"The new User-Agent string";
    }
    [self newSetValue:value forHTTPHeaderField:field];
}

+ (void)setupUserAgentOverwrite
{
    [self swizzleMethod:@selector(setValue:forHTTPHeaderField:)
            withMethod:@selector(newSetValue:forHTTPHeaderField:)];
}

@end</pre>
<p>This new method is implemented as a category, we don&#8217;t need to subclass. The replacement method for &#8220;setValue:forHTTPHeaderField:&#8221; is called &#8220;newSetValue:forHTTPHeaderField:&#8221; and it is simply checking if the &#8220;field&#8221; variable is equal to &#8220;User-Agent&#8221;. If it is, the value is modified. Afterwards the original method is called.<br />
Please note: because the method swizzling exchanges the original and replacement methods, we have to call &#8220;newSetValue:forHTTPHeaderField:&#8221; to call the original method &#8220;setValue:forHTTPHeaderField:&#8221;. This looks confusing, but this is the way you can give control back to the original method.</p>
<p>The method &#8220;setupUserAgentOverwrite&#8221; has to be called once after the App is launched (for example in the Application delegate in the &#8220;applicationDidFinishLaunching:&#8221; method, or even in &#8220;main()&#8221;).</p>
<pre style="padding-left: 0.7em; border-left: 1px solid #030; color: #003300; font-size: 0.9em;">   [NSMutableURLRequest setupUserAgentOverwrite];</pre>
<p>This should be done before any UIWebView objects are created to make sure that the &#8220;User-Agent&#8221; is modified for all requests.</p>
<p>You can also use this approach when you need to modify other HTTP headers.</p>
]]></content:encoded>
			<wfw:commentRss>http://www.icab.de/blog/2010/04/07/changing-the-headers-for-uiwebkit-http-requests/feed/</wfw:commentRss>
		<slash:comments>31</slash:comments>
		</item>
		<item>
		<title>Search and highlight text in UIWebView</title>
		<link>http://www.icab.de/blog/2010/01/12/search-and-highlight-text-in-uiwebview/</link>
		<comments>http://www.icab.de/blog/2010/01/12/search-and-highlight-text-in-uiwebview/#comments</comments>
		<pubDate>Mon, 11 Jan 2010 23:26:20 +0000</pubDate>
		<dc:creator>Alexander</dc:creator>
				<category><![CDATA[Programming]]></category>
		<category><![CDATA[iPhone & iPod Touch]]></category>
		<category><![CDATA[Cocoa]]></category>
		<category><![CDATA[Development]]></category>
		<category><![CDATA[iPhone]]></category>
		<category><![CDATA[JavaScript]]></category>
		<category><![CDATA[WebKit]]></category>

		<guid isPermaLink="false">http://www.icab.de/blog/?p=110</guid>
		<description><![CDATA[Several iPhone Apps (like my &#8220;iCab Mobile&#8221; or &#8220;NewsTap&#8221; Apps) provide a search feature which allows to search for text in the content that is currently displayed within a UIWebView. The found occurrences of the searched text are highlighted with a yellow background, so the search result can be visually located very easy. This blog [...]]]></description>
			<content:encoded><![CDATA[<p>Several iPhone Apps (like my &#8220;iCab Mobile&#8221; or &#8220;NewsTap&#8221; Apps) provide a search feature which allows to search for text in the content that is currently displayed within a UIWebView. The found occurrences of the searched text are highlighted with a yellow background, so the search result can be visually located very easy.</p>
<p>This blog post describes how this can be implemented. I&#8217;m implementing this feature as a category for the UIWebView class, so you can use the new search feature for all UIWebView objects in your Apps very easily.</p>
<p>First of all, UIWebView doesn&#8217;t allow us to access its content directly, so we have to use JavaScript again. But if you&#8217;ve read my other blog posts, you already know this approach.</p>
<p>Our goal is to implement two methods for our new UIWebView category. One method should start the search and highlights the search results. As a result this method should return the number of occurrences of the text we&#8217;ve searched. The other method should remove all the highlighted search results again to restore the original layout of the web page.</p>
<p>What we need to do is to write the main code in JavaScript and a wrapper code in Objective C which is simply calling the JavaScript code.</p>
<p>We start with the JavaScript code that is doing the real work. As I&#8217;ve already described in the blog post <a href="http://www.icab.de/blog/2009/07/27/webkit-on-the-iphone-part-1/"> WebKit on the iPhone</a>, the JavaScript code will be saved as resource file in the XCode project. This way it can be loaded from within the Objective C code from the application bundle very easily, and we don&#8217;t mix the code of multiple programming languages (JavaScript and Objective C) in the same files.</p>
<p>The following code is the the JavaScript implementation; below I&#8217;ll explain what it is doing and how it works:</p>
<p><span style="color: #993300;">SearchWebView.js:</span></p>
<pre style="padding-left: 0.7em; border-left: 1px solid #030; color: #003300; font-size: 0.9em;">// We're using a global variable to store the number of occurrences
var MyApp_SearchResultCount = 0;

// helper function, recursively searches in elements and their child nodes
function MyApp_HighlightAllOccurencesOfStringForElement(element,keyword) {
  if (element) {
    if (element.nodeType == 3) {        // Text node
      while (true) {
        var value = element.nodeValue;  // Search for keyword in text node
        var idx = value.toLowerCase().indexOf(keyword);

        if (idx &lt; 0) break;             // not found, abort

        var span = document.createElement("span");
        var text = document.createTextNode(value.substr(idx,keyword.length));
        span.appendChild(text);
        span.setAttribute("class","MyAppHighlight");
        span.style.backgroundColor="yellow";
        span.style.color="black";
        text = document.createTextNode(value.substr(idx+keyword.length));
        element.deleteData(idx, value.length - idx);
        var next = element.nextSibling;
        element.parentNode.insertBefore(span, next);
        element.parentNode.insertBefore(text, next);
        element = text;
        MyApp_SearchResultCount++;	// update the counter
      }
    } else if (element.nodeType == 1) { // Element node
      if (element.style.display != "none" &amp;&amp; element.nodeName.toLowerCase() != 'select') {
        for (var i=element.childNodes.length-1; i&gt;=0; i--) {
          MyApp_HighlightAllOccurencesOfStringForElement(element.childNodes[i],keyword);
        }
      }
    }
  }
}

// the main entry point to start the search
function MyApp_HighlightAllOccurencesOfString(keyword) {
  MyApp_RemoveAllHighlights();
  MyApp_HighlightAllOccurencesOfStringForElement(document.body, keyword.toLowerCase());
}

// helper function, recursively removes the highlights in elements and their childs
function MyApp_RemoveAllHighlightsForElement(element) {
  if (element) {
    if (element.nodeType == 1) {
      if (element.getAttribute("class") == "MyAppHighlight") {
        var text = element.removeChild(element.firstChild);
        element.parentNode.insertBefore(text,element);
        element.parentNode.removeChild(element);
        return true;
      } else {
        var normalize = false;
        for (var i=element.childNodes.length-1; i&gt;=0; i--) {
          if (MyApp_RemoveAllHighlightsForElement(element.childNodes[i])) {
            normalize = true;
          }
        }
        if (normalize) {
          element.normalize();
        }
      }
    }
  }
  return false;
}

// the main entry point to remove the highlights
function MyApp_RemoveAllHighlights() {
  MyApp_SearchResultCount = 0;
  MyApp_RemoveAllHighlightsForElement(document.body);
}</pre>
<p>The basic principle of searching the text and removing the highlighted search results is the same: We&#8217;re working at DOM level (Document Object Model), which means the HTML document is represented as a tree structure where each HTML element, text, comment etc. is represented as a node. All the nodes are linked together with parent and child connections. The root element of each HTML document is the element that is created by the HTML tag. This element has usually two children: The HEAD element and the BODY element. Only the content of the BODY element is visible and displayed on screen, so we only need to process this part of the document tree.</p>
<p>What we need to do is to start with the body element and traverse all of its child nodes. From within the child nodes we need to go to their child nodes as well, and so on until we reach a leaf nodes, which has no child elements. Text nodes are always leaf nodes and text nodes are the nodes which might contain the text we&#8217;re looking for.</p>
<p>Traversing the whole HTML tree searching for all text nodes can be done by a recursive algorithm called Depth-First-Search (DFS). The DFS algorithm will traverse the tree structure starting from a root element (in our case the BODY element) to the first leaf node in a branch of the tree (for example going to the first child of the root first, from there again going to the first child, etc until a leaf node is reached). Then the algorithm goes back (backtracking) to the last node where not all child nodes were traversed yet and continues with the next unvisited child nodes etc. This way all nodes of the whole tree are traversed and we are able to find all the text nodes in which we are looking for the text we are earching.</p>
<p>The functions &#8220;MyApp_HighlightAllOccurencesOfStringForElement(element,keyword)&#8221; and &#8220;MyApp_RemoveAllHighlightsForElement(element)&#8221; are both implementations of this DFS algorithm. These functions are called from MyApp_HighlightAllOccurencesOfString(keyword)&#8221; and &#8220;MyApp_RemoveAllHighlights()&#8221; which are doing the necessary initialization and provide the DFS functions with the proper root element (the BODY element). The initialization for a new search is to make sure than no highlighted text from a previous search is present, so we simple call the function to remove all text highlights.</p>
<p>When searching for a text, we check if the currently inspected node is a text node or if it is an element node. If it is an element node it can have child nodes, and these must be inspected as well. If it is a text node, we have to find out if the text of this node contains the text we&#8217;re searching. If yes, we insert the text highlight, otherwise we are finished with this node. Also if the node is neither a text node nor an element node, there aren&#8217;t any child nodes which are interesting for us, so we are finished with this node as well.</p>
<p>When the text of a text node contains the searched text, we have to split the text into three parts. Part one will contain the text up to the searched text, part two contains the searched text itself and part three contains the rest of the text. A new element will be created (a SPAN element) and the second part (the searched text) will become a child node of this new element. Now we can assign StyleSheet rules to the newly created SPAN element to create the highlight effect (setting the background color to yellow, setting the text color to black, you can even increase the font size, if you want to). Now the new element is linked with part one and three so it becomes a part of the tree strucuture of the HTML tree. And because the searched text might be found multiple times in the original text node, we continue to search for the searched text in the third part of the original text node. If we find another occurrence of the searched text, we split this third part again in three parts, otherwise we are finished. When we create a SPAN element for the highlight effect, we also assign a special value (here &#8220;MyAppHighlight&#8221;) for the  class attribute. This is important to be able to find these elements later again when we want to remove the highlight effects using the function &#8220;MyApp_RemoveAllHighlights()&#8221;. For this task we traverse the tree as well, but now we&#8217;re looking for elements whose class attribute has this special value. To restore the original state of the HTML document, we have to remove the elements we&#8217;ve inserted before (the ones with the special value of the class attribute) and we need to concatenate the text node we&#8217;ve split. JavaScript can help us to concatenate the text nodes again, because it provides the &#8220;normalize()&#8221; function which can do this for us.</p>
<p>In JavaScript we can find out the type of a node with the &#8220;nodeType&#8221; property. A value of 1 means that the node is a normal element node (like the &#8220;body&#8221; node, a &#8220;span&#8221; node etc.). A value of 3 means that the node is a text node. In this case the property nodeValue contains the text of the node. Other values for nodeType represent comment nodes (in HTML these are written as &#8220;&lt;!&#8211; Comment &#8211;&gt;&#8221;), attribute nodes (for HTML attributes like for example the &#8220;HREF&#8221; attribute for the &#8220;A&#8221; tag), document nodes and some more. In our case only the values 1 (element node) and 3 (text node) are important.</p>
<p>In the above implementation, we count the number of found occurrences in a global variable.</p>
<p><strong>Note:</strong> You&#8217;ll notice that the JavaScript function names and variables and also the value for the class attribute I&#8217;m using in the above code are very lengthy and they do also have a prefix like &#8220;MyApp_&#8221;. The reason for this is to avoid any conflicts with existing function and variable names of the web page in which we inject our JavaScript code. If you&#8217;re generating the HTML code yourself that is displayed in the UIWebView object, you can choose shorter and simpler names. But if you have to deal with HTML and JavaScript code of any web pages (like in a web browser like iCab Mobile), you should use longer names and also add the name of your app as Prefix to all function and variable names to avoid any conflicts.</p>
<p>The Cocoa/Objective C part of the implementation is very simple. We only need to declare the interface and write a simple wrapper which loads and calls the JavaScript code that is actually doing all the hard work. The interface is also simple, we only need two methods: one to start the search and which highlights the found text and one which removes all the highlights again.</p>
<p><span style="color: #993300;">SearchWebView.h:</span></p>
<pre style="padding-left: 0.7em; border-left: 1px solid #030; color: #003300; font-size: 0.9em;">@interface UIWebView (SearchWebView)

- (NSInteger)highlightAllOccurencesOfString:(NSString*)str;
- (void)removeAllHighlights;

@end</pre>
<p>The typical use case would be to provide a search field where the user can enter some text. This text would be passed to the method &#8220;highlightAllOccurencesOfString:&#8221;. And when the user shakes the device, the App could call the method &#8220;removeAllHighlights&#8221; to remove all the highlighted search results again.</p>
<p>The implementation would look like this:</p>
<p><span style="color: #993300;">SearchWebView.m:</span></p>
<pre style="padding-left: 0.7em; border-left: 1px solid #030; color: #003300; font-size: 0.9em;">@implementation UIWebView (SearchWebView)

- (NSInteger)highlightAllOccurencesOfString:(NSString*)str
{
    NSString *path = [[NSBundle mainBundle] pathForResource:@"SearchWebView" ofType:@"js"];
    NSString *jsCode = [NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:nil];
    [self stringByEvaluatingJavaScriptFromString:jsCode];

    NSString *startSearch = [NSString stringWithFormat:@"MyApp_HighlightAllOccurencesOfString('%@')",str];
    [self stringByEvaluatingJavaScriptFromString:startSearch];

    NSString *result = [self stringByEvaluatingJavaScriptFromString:@"MyApp_SearchResultCount"];
    return [result integerValue];
}

- (void)removeAllHighlights
{
    [self stringByEvaluatingJavaScriptFromString:@"MyApp_RemoveAllHighlights()"];
}

@end</pre>
<p>The first thing we&#8217;re doing in the method &#8220;highlightAllOccurencesOfString:&#8221; is to load the JavaScript file we&#8217;ve written above from the application bundle and inject it into the web page that is currently displayed in UIWebView. Because we&#8217;re implementing this as a category for UIWebView, we can use &#8220;self&#8221; to call the method &#8220;stringByEvaluatingJavaScriptFromString:&#8221; of the UIWebView instances.</p>
<p>After we&#8217;ve injected the JavaScript code we simply call the JavaScript function we&#8217;ve defined above to do the search.</p>
<p>And finally we access the variable we&#8217;ve defined in the JavaScript code, which represents the number of occurrences of the string that were found, and we return its integer value as the result of the method.</p>
<p>In the method &#8220;removeAllHighlights&#8221; we only need to call the  corresponding JavaScript function we&#8217;ve defined in the JavaScript code from above. Loading the external JavaScript file and injecting it into the Web page is not necessary here. If we&#8217;ve started a search before, the code is already injected and we don&#8217;t need to do this again. And if we haven&#8217;t started a search before, we just don&#8217;t need the JavaScript code because there are no highlights which have to be removed.</p>
<p>As you can see, the Objective C code for the UIWebView category is just a simple wrapper code for the JavaScript code. Now, when you have a UIWebView object in your App, you can simply search for text in its content by calling &#8220;highlightAllOccurencesOfString:&#8221;, like in this example where we&#8217;re searching for the text &#8220;keyword&#8221;:</p>
<pre style="padding-left: 0.7em; border-left: 1px solid #030; color: #003300; font-size: 0.9em;">  // webView is an instance of UIWebView
  [webView highlightAllOccurencesOfString:@"keyword"];</pre>
<p>Additional notes: In case your App has to deal with web pages which can have frames, you have to add some additional code that looks for frames. You have to traverse all the documents of all the frames to find all the text nodes in all frames. The code from above isn&#8217;t doing this to keep it as simple as possible.</p>
]]></content:encoded>
			<wfw:commentRss>http://www.icab.de/blog/2010/01/12/search-and-highlight-text-in-uiwebview/feed/</wfw:commentRss>
		<slash:comments>77</slash:comments>
		</item>
		<item>
		<title>URL filtering for UIWebView on the iPhone</title>
		<link>http://www.icab.de/blog/2009/08/18/url-filtering-with-uiwebview-on-the-iphone/</link>
		<comments>http://www.icab.de/blog/2009/08/18/url-filtering-with-uiwebview-on-the-iphone/#comments</comments>
		<pubDate>Tue, 18 Aug 2009 21:10:10 +0000</pubDate>
		<dc:creator>Alexander</dc:creator>
				<category><![CDATA[Programming]]></category>
		<category><![CDATA[iPhone & iPod Touch]]></category>
		<category><![CDATA[Cocoa]]></category>
		<category><![CDATA[Development]]></category>
		<category><![CDATA[iPhone]]></category>
		<category><![CDATA[WebKit]]></category>

		<guid isPermaLink="false">http://www.icab.de/blog/?p=35</guid>
		<description><![CDATA[iCab Mobile provides a filter manager which allows to filter out advertising banners and other stuff from web pages. It has a list of simple URL-based filter rules (which is even editable by the user) and when a web page contains resources (image files, JavaScript files, stylesheets etc.) whose URLs match one of these rules, [...]]]></description>
			<content:encoded><![CDATA[<p>iCab Mobile provides a filter manager which allows to filter out advertising banners and other stuff from web pages. It has a list of simple URL-based filter rules (which is even editable by the user) and when a web page contains resources (image files, JavaScript files, stylesheets etc.) whose URLs match one of these rules, the resources won&#8217;t be loaded.</p>
<p>But implementing filters seems to be impossible. When you look at the public API of the UIWebView class, you won&#8217;t see anything which would allow to find out which resources the UIWebView object is loading, and even worse, nothing is available which can be used to force the UIWebView not to load these resources when you want to filter them out.</p>
<p>But of course there is a solution, otherwise this blog post wouldn&#8217;t make much sense <img src='http://www.icab.de/blog/wp-includes/images/smilies/icon_wink.gif' alt=';-)' class='wp-smiley' /> .</p>
<p>To implement filters we don&#8217;t have to look at UIWebView. As I mentioned above, nothing in the UIWebView API would allow to implement filtering.</p>
<p>To find a hook where we can intercept all the HTTP requests which are done by UIWebView we have to know a little bit about the URL loading system of Cocoa because UIWebView is using the URL loading system to get all the data from the web. One part of the URL loading system is the NSURLCache class, and this is our hook we&#8217;re looking for. Though the iPhone OS doesn&#8217;t cache any data on &#8220;disk&#8221; at the moment (this can be different in later iPhone OS release) and therefore the cache that is managed by the NSURLCache class is usually empty, UIWebView nevertheless checks if the requested resources are in the cache. So all we need to do is to subclass NSURLCache and overwrite the method</p>
<pre style="padding-left:0.7em; border-left: 1px solid #030; color: #003300; font-size:0.9em">- (NSCachedURLResponse*)cachedResponseForRequest:(NSURLRequest*)request</pre>
<p>This method is called for all resources the UIWebView is requesting. So all we need to do is to check if the URL of the request matches one of the filters. If it does, we create a fake response with no content, otherwise we just call the super class.  This is basically all we need to do.</p>
<p>Here&#8217;re some more details:</p>
<p><strong>1. Subclassing NSURLCache:</strong><br />
In the Header file there&#8217;s almost nothing to do:</p>
<p><span style="color: #993300;"> FilteredWebCache.h:</span></p>
<pre style="padding-left:0.7em; border-left: 1px solid #030; color: #003300; font-size:0.9em">@interface FilteredWebCache : NSURLCache
{
}
@end</pre>
<p>Now the main code for the subclass:</p>
<p><span style="color: #993300;"> FilteredWebCache.m:</span></p>
<pre style="padding-left:0.7em; border-left: 1px solid #030; color: #003300; font-size:0.9em">#import "FilteredWebCache.h"
#import "FilterManager.h"

@implementation FilteredWebCache

- (NSCachedURLResponse*)cachedResponseForRequest:(NSURLRequest*)request
{
    NSURL *url = [request URL];
    BOOL blockURL = [[FilterMgr sharedFilterMgr] shouldBlockURL:url];
    if (blockURL) {
        NSURLResponse *response =
              [[NSURLResponse alloc] initWithURL:url
                                        MIMEType:@"text/plain"
                           expectedContentLength:1
                                textEncodingName:nil];

        NSCachedURLResponse *cachedResponse =
              [[NSCachedURLResponse alloc] initWithResponse:response
                             data:[NSData dataWithBytes:" " length:1]];

        [super storeCachedResponse:cachedResponse forRequest:request];

        [cachedResponse release];
        [response release];
    }
    return [super cachedResponseForRequest:request];
}
@end</pre>
<p>The code first checks if the URL should be blocked (the FilterManager class is doing all these checks, this class isn&#8217;t shown here). If yes, it creates a new response object with no content and stores this in the cache. One could assume that it should be possible to just return the fake response object and we don&#8217;t need to store it in the cache. But if we do this, the app would crash very soon because our fake response object is over-released by the iPhone OS. I don&#8217;t know why exactly this happens, this might be a bug in the iPhone OS (and also in MacOSX 10.5.x where the same thing happens. This works fine in 10.4.x and all older MacOSX releases) or caused by some undocumented internal dependencies between the different classes of the URL loading system. So we just store our fake response in the Cache. This makes sure that all response objects we return are really stored in the Cache and this is what the iPhone OS expects and then it won&#8217;t crash.<br />
<br /><b>Update:</b> It seems that it is also necessary that the &#8220;fake&#8221; response is initialized with a NSData object which has a size larger than zero. </p>
<p><strong>2. Creating a new Cache:</strong><br />
We also need to create a new cache and tell the iPhone OS that it has to use this new cache instead of the default one so we really get called when the URL loading system checks the cache for a resource. This should be done before any of the UIWebView objects are starting to load web pages, very early within the launching process of the app.</p>
<pre style="padding-left:0.7em; border-left: 1px solid #030; color: #003300; font-size:0.9em">NSString *path = ...// the path to the cache file
NSUInteger discCapacity = 10*1024*1024;
NSUInteger memoryCapacity = 512*1024;

FilteredWebCache *cache =
      [[FilteredWebCache alloc] initWithMemoryCapacity: memoryCapacity
                             diskCapacity: discCapacity diskPath:path];
[NSURLCache setSharedURLCache:cache];
[cache release];</pre>
<p>We have to provide a path where the cache file is stored. The cache file is automatically created by the NSURLCache objects, so we don&#8217;t need to create the file, we only have to define where the file should be saved (this must be somewhere in the &#8220;sandbox&#8221; of our application, for example in the &#8220;tmp&#8221; folder or in the &#8220;Documents&#8221; folder).</p>
<p>This is all the magic to implement URL-based filters for UIWebView on the iPhone. You see, it&#8217;s not that complicated.</p>
<p>Note: If the filters can change while your app is running, you may need to remove the fake responses from the cache again. The NSURLCache class provides a method for this, so this isn&#8217;t a problem. If your filters are static, then you don&#8217;t need to care about this.</p>
]]></content:encoded>
			<wfw:commentRss>http://www.icab.de/blog/2009/08/18/url-filtering-with-uiwebview-on-the-iphone/feed/</wfw:commentRss>
		<slash:comments>42</slash:comments>
		</item>
		<item>
		<title>WebKit on the iPhone (Part 2)</title>
		<link>http://www.icab.de/blog/2009/08/05/webkit-on-the-iphone-part-2/</link>
		<comments>http://www.icab.de/blog/2009/08/05/webkit-on-the-iphone-part-2/#comments</comments>
		<pubDate>Wed, 05 Aug 2009 14:56:58 +0000</pubDate>
		<dc:creator>Alexander</dc:creator>
				<category><![CDATA[Programming]]></category>
		<category><![CDATA[iPhone & iPod Touch]]></category>
		<category><![CDATA[Cocoa]]></category>
		<category><![CDATA[Development]]></category>
		<category><![CDATA[iPhone]]></category>
		<category><![CDATA[WebKit]]></category>

		<guid isPermaLink="false">http://www.icab.de/blog/?p=21</guid>
		<description><![CDATA[In the first part of this tutorial you&#8217;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 [...]]]></description>
			<content:encoded><![CDATA[<p>In the first part of this tutorial you&#8217;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.</p>
<p>In the second part of the tutorial you&#8217;ll learn how to open links (which are supposed to open in a new window) in a new tab. On the iPhone you can&#8217;t open multiple windows, so we have to use &#8220;tabs&#8221; instead of windows. I&#8217;ll also show how to deal with opening windows or tabs via JavaScript when no HTML link is involved.</p>
<p>In part one we&#8217;ve written the JavaScript function &#8220;<span style="color: #003300; font-family:courier; font-size:0.9em">MyIPhoneApp_ModifyLinkTargets()</span>&#8221; which loops through all links and replaces the link target &#8220;_blank&#8221; with &#8220;_self&#8221;. 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&#8217;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 &#8220;newtab&#8221;. We just add this to the existing JavaScript function we&#8217;ve written in the last part of the tutorial.</p>
<pre style="padding-left:0.7em; border-left: 1px solid #030; color: #003300; font-size:0.9em">function MyIPhoneApp_ModifyLinkTargets() {
    var allLinks = document.getElementsByTagName('a');
    if (allLinks) {
        var i;
        for (i=0; i&lt;allLinks.length; i++) {
            var link = allLinks[i];
            var target = link.getAttribute('target');
            if (target &amp;&amp; target == '_blank') {
                link.setAttribute('target','_self');
                link.href = 'newtab:'+escape(link.href);
            }
        }
    }
}</pre>
<p>The additional line</p>
<pre style="padding-left:0.7em; border-left: 1px solid #030; color: #003300; font-size:0.9em">link.href = 'newtab:'+escape(link.href);</pre>
<p>will take the original URL and escapes all the characters with a special meaning (like &#8220;:&#8221; and &#8220;/&#8221;) and puts the new custom URL scheme &#8220;newtab&#8221; in front of it. The link URL &#8220;http://www.apple.com/&#8221; will be converted to &#8220;newtab:http%3A//www.apple.com/&#8221; for example.</p>
<p>Within the Objective-C code of our app, we have to implement the UIWebView delegate method</p>
<pre style="padding-left:0.7em; border-left: 1px solid #030; color: #003300; font-size:0.9em">- (BOOL)webView:(UIWebView *)view shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType</pre>
<p>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 &#8220;newtab&#8221;. So the delegate method would look like this:</p>
<pre style="padding-left:0.7em; border-left: 1px solid #030; color: #003300; font-size:0.9em">- (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;
}</pre>
<p>The &#8220;<span style="color: #003300; font-family:courier; font-size:0.9em">resourceSpecifier</span>&#8221; 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&#8217;ve escaped the original URL we need to unescape it. And this can be done with &#8220;<span style="color: #003300; font-family:courier; font-size:0.9em">stringByReplacingPercentEscapesUsingEncoding:</span>&#8220;. The method &#8220;<span style="color: #003300; font-family:courier; font-size:0.9em">openNewTabWithURL:</span>&#8221; 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.</p>
<p>In the above code you&#8217;ll notice this line:</p>
<pre style="padding-left:0.7em; border-left: 1px solid #030; color: #003300; font-size:0.9em">url = [NSURL URLWithString:urlString relativeToURL:[view url]];</pre>
<p>We call &#8220;<span style="color: #003300; font-family:courier; font-size:0.9em">[view url]</span>&#8221; here (calling the method we&#8217;ve written in the first part of the tutorial) to get the base URL, in case we&#8217;ve received only a relative URL from the JavaScript code. For normal links this wouldn&#8217;t be necessary because the &#8220;href&#8221; 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.</p>
<p>And now it&#8217;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.</p>
<p>In JavaScript there&#8217;s the call</p>
<pre style="padding-left:0.7em; border-left: 1px solid #030; color: #003300; font-size:0.9em">window.open(url,target,parameters)</pre>
<p>Where &#8220;url&#8221; is the URL to open, &#8220;target&#8221; is the target, similar to the target attribute of HTML links and &#8220;parameters&#8221; 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 &#8220;<span style="color: #003300; font-family:courier; font-size:0.9em">window.open()</span>&#8221; on the iPhone with a target that is a new window, nothing will happen. This is because the UIWebView object doesn&#8217;t support new windows or tabs. So we need to overwrite the the original &#8220;<span style="color: #003300; font-family:courier; font-size:0.9em">window.open()</span>&#8221; function of JavaScript so our own code is executed whenever &#8220;<span style="color: #003300; font-family:courier; font-size:0.9em">window.open()</span>&#8221; 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 &#8220;newtab&#8221;, like we&#8217;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 &#8220;<span style="color: #003300; font-family:courier; font-size:0.9em">location.href</span>&#8220;.</p>
<pre style="padding-left:0.7em; border-left: 1px solid #030; color: #003300; font-size:0.9em">function MyIPhoneApp_ModifyWindowOpen() {
    window.open =
            function(url,target,param) {
                if (url &amp;&amp; url.length &gt; 0) {
                    if (!target) target = "_blank";
                    if (target == '_blank') {
                        location.href = 'newtab:'+escape(url);
                    } else {
                        location.href = url;
                    }
                }
            }
}</pre>
<p>Please note that this code makes certain assumptions: There are no HTML frames used in the web page and the only target names are &#8220;_blank&#8221;, &#8220;_self&#8221;, &#8220;_top&#8221;, &#8220;_parent&#8221;. Dealing with frames requires some extra work and would make things more complicated. So this should be handled in a later tutorial.</p>
<p>In the delegate method &#8220;webViewDidFinishLoad:&#8221; we just need to call the JavaScript function &#8220;<span style="color: #003300; font-family:courier; font-size:0.9em">MyIPhoneApp_ModifyWindowOpen()</span>&#8221; as well. So we add a new line of code:</p>
<pre style="padding-left:0.7em; border-left: 1px solid #030; color: #003300; font-size:0.9em">- (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()"];
}</pre>
<p>Now, whenever a web page uses the JavaScript call &#8220;<span style="color: #003300; font-family:courier; font-size:0.9em">window.open()</span>&#8220;, to open a page in a new window, the delegate method &#8220;<span style="color: #003300; font-family:courier; font-size:0.9em">webView:shouldStartLoadWithRequest:navigationType:</span>&#8221; is called, just like it is called when a link is opened. And because we&#8217;ve created a new URL with a &#8220;newtab&#8221; URL scheme when necessary, this is automtatically detected by the code we&#8217;ve written before.</p>
<p>What you&#8217;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 &#8220;window.open()&#8221;. 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&#8217;s not too difficult.</p>
<p>Final notes:</p>
<p>There are still some details which are not covered by this tutorial. For example web pages can have &#8220;frames&#8221; and the targets referenced by links and &#8220;window.open()&#8221; can also address these frames. So you would need some additional code to check if a frame with the target name exists. If there&#8217;s a frame with the target name, then do not modify the link, if no frame exists, you should modify the link (using the &#8220;newtab&#8221; scheme) and treat the target name just like &#8220;_blank&#8221;.</p>
<p>Also &#8220;window.open()&#8221; 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&#8217;t need to access the new windows later, so this limitation is usually not a big deal.</p>
]]></content:encoded>
			<wfw:commentRss>http://www.icab.de/blog/2009/08/05/webkit-on-the-iphone-part-2/feed/</wfw:commentRss>
		<slash:comments>29</slash:comments>
		</item>
		<item>
		<title>WebKit on the iPhone (Part 1)</title>
		<link>http://www.icab.de/blog/2009/07/27/webkit-on-the-iphone-part-1/</link>
		<comments>http://www.icab.de/blog/2009/07/27/webkit-on-the-iphone-part-1/#comments</comments>
		<pubDate>Mon, 27 Jul 2009 17:04:31 +0000</pubDate>
		<dc:creator>Alexander</dc:creator>
				<category><![CDATA[Programming]]></category>
		<category><![CDATA[iPhone & iPod Touch]]></category>
		<category><![CDATA[Cocoa]]></category>
		<category><![CDATA[Development]]></category>
		<category><![CDATA[iPhone]]></category>
		<category><![CDATA[WebKit]]></category>

		<guid isPermaLink="false">http://www.icab.de/blog/?p=14</guid>
		<description><![CDATA[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 [...]]]></description>
			<content:encoded><![CDATA[<p>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.</p>
<p>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&#8217;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&#8217;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&#8217;t enough, many essential features are missing.</p>
<p>Some examples:</p>
<ul>
<li>UIWebView doesn&#8217;t provide a method to get the title of the currently displayed web page,</li>
<li>it just ignores all attempts to open links which are meant to open in new windows or tabs</li>
<li>it doesn&#8217;t allow accessing the HTML tree</li>
</ul>
<p>WebKit itself provides many classes for all these tasks, but all of them are private and not available on the iPhone.</p>
<p>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 &#8220;no anoying popup window&#8221;). This sounds great, but of course this doesn&#8217;t make such browsers useful in the real world.</p>
<p>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&#8217;t. But we can implement many of the missing feature.</p>
<p>If you look at the available methods, there&#8217;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</p>
<pre style="padding-left:0.7em; border-left: 1px solid #030; color: #003300; font-size:0.9em">- (NSString *)stringByEvaluatingJavaScriptFromString:(NSString *)script;</pre>
<p>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.</p>
<p>Let&#8217;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 &#8220;Category&#8221;, so we don&#8217;t need to subclass UIWebView:</p>
<p>File: <span style="color: #993300;">MyWebViewAdditions.h</span></p>
<pre style="padding-left:0.7em; border-left: 1px solid #030; color: #003300; font-size:0.9em">@interface UIWebView (MyWebViewAdditions)
- (NSString*)title;
- (NSURL*)url;
@end</pre>
<p>File: <span style="color: #993300;">MyWebViewAdditions.m</span></p>
<pre style="padding-left:0.7em; border-left: 1px solid #030; color: #003300; font-size:0.9em">#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</pre>
<p>What are we doing here?<br />
From the JavaScript&#8217;s point of view a web page is represented by the &#8220;<strong>document</strong>&#8221; object which has several properties. One of the properties is the &#8220;<strong>title</strong>&#8221; property which contains the title of the page. So with &#8220;<strong>document.title</strong>&#8221; 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 &#8220;<span style="color: #003300; font-family:courier; font-size:0.9em">stringByEvaluatingJavaScriptFromString:</span>&#8221; to get the document title.</p>
<p>For retreiving the URL we do something similar.</p>
<p>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 &#8220;<span style="color: #003300; font-family:courier; font-size:0.9em">title</span>&#8221; or &#8220;<span style="color: #003300; font-family:courier; font-size:0.9em">url</span>&#8221; method:</p>
<pre style="padding-left:0.7em; border-left: 1px solid #030; color: #003300; font-size:0.9em">NSString *title = [anyUIWebViewObject title];</pre>
<p>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&#8217;t support such a delegate method and so all attempts to open such a link are just ignored.</p>
<p>These links do usually look like this:</p>
<pre style="padding-left:0.7em; border-left: 1px solid #030; color: #003300; font-size:0.9em">&lt;a href="destination" target="_blank"&gt;Link Text&lt;/a&gt;</pre>
<p>The &#8220;<strong>target</strong>&#8221; 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 &#8220;<strong>_blank</strong>&#8221; (opens a new window), &#8220;<strong>_self</strong>&#8221; (the window itself), &#8220;<strong>_parent</strong>&#8221; (the parent frame, if there are nested frames) and &#8220;<strong>_top</strong>&#8221; (the top-level or root frame, or identical to &#8220;<strong>_self</strong>&#8221; if the page doesn&#8217;t use frames).</p>
<p>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 &#8220;<strong>target</strong>&#8221; attribute set to &#8220;<strong>_blank</strong>&#8221; and change its value to &#8220;<strong>_self</strong>&#8220;. 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</p>
<pre style="padding-left:0.7em; border-left: 1px solid #030; color: #003300; font-size:0.9em">- (void)webViewDidFinishLoad:(UIWebView *)webView;</pre>
<p>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 &#8220;<span style="color: #003300; font-family:courier; font-size:0.9em">stringByEvaluatingJavaScriptFromString:</span>&#8220;).</p>
<p>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&#8217;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:</p>
<p>File: <span style="color: #993300;">ModifyLinkTargets.js</span>:</p>
<pre style="padding-left:0.7em; border-left: 1px solid #030; color: #003300; font-size:0.9em">function MyIPhoneApp_ModifyLinkTargets() {
    var allLinks = document.getElementsByTagName('a');
    if (allLinks) {
        var i;
        for (i=0; i&lt;allLinks.length; i++) {
            var link = allLinks[i];
            var target = link.getAttribute('target');
            if (target &amp;&amp; target == '_blank') {
                link.setAttribute('target','_self');
            }
        }
    }
}</pre>
<p>What is this JavaScript function doing, when called?<br />
It gets an array of all links (&#8220;<strong>a</strong>&#8221; tags) and then loops through all of these tags, checks if there&#8217;s a target attribute with the value &#8220;<strong>_blank</strong>&#8220;. If this is the case it changes the value to &#8220;<strong>_self</strong>&#8220;.</p>
<p>Note: There are other tags which can have a &#8220;<strong>target</strong>&#8221; attribute, like the &#8220;<strong>form</strong>&#8221; tag and the &#8220;<strong>area</strong>&#8221; tag. So you can use the &#8220;<span style="color: #003300; font-family:courier; font-size:0.9em">getElementsByTagName()</span>&#8221; call to get these tags as well and modify their target attributes in the same way as I&#8217;ve done this for the &#8220;<strong>a</strong>&#8221; tag.</p>
<p>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:</p>
<pre style="padding-left:0.7em; border-left: 1px solid #030; color: #003300; font-size:0.9em">- (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()"];
}</pre>
<p>What is this code doing?<br />
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<br />
which is modifying the link targets.</p>
<p>Some notes:</p>
<ul>
<li>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.</li>
<li>Using a long name for our JavaScript function which also includes a prefix like &#8220;<span style="color: #003300; font-family:courier; font-size:0.9em">MyIPhoneApp_</span>&#8221; 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&#8217;t created ourselves and where we can not predict which function or variable names the JavaScript code of the web page is already using.</li>
<li>Using separate calls of &#8220;<span style="color: #003300; font-family:courier; font-size:0.9em">stringByEvaluatingJavaScriptFromString</span>&#8221; 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&#8217;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.</li>
<li>The delegate method &#8220;<span style="color: #003300; font-family:courier; font-size:0.9em">webViewDidFinishLoad:(UIWebView *)webView</span>&#8221; 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.</li>
</ul>
<p>What next?</p>
<ul>
<li>The above example code does not cover web pages where new windows are opened using JavaScript.</li>
<li>The links will open in the same window, which is fine because they are no longer ignored. But they still don&#8217;t open in a new window or Tab.</li>
</ul>
<p>More about this topic and the cases which are not yet covered will come in the second part of the &#8220;WebKit on the iPhone&#8221; article.</p>
<p>Feel free to ask questions and write comments. I&#8217;d like to get some feedback.</p>
]]></content:encoded>
			<wfw:commentRss>http://www.icab.de/blog/2009/07/27/webkit-on-the-iphone-part-1/feed/</wfw:commentRss>
		<slash:comments>63</slash:comments>
		</item>
		<item>
		<title>iCab running on WebKit nightly</title>
		<link>http://www.icab.de/blog/2009/07/26/icab-running-on-webkit-nightly/</link>
		<comments>http://www.icab.de/blog/2009/07/26/icab-running-on-webkit-nightly/#comments</comments>
		<pubDate>Sat, 25 Jul 2009 22:37:23 +0000</pubDate>
		<dc:creator>Alexander</dc:creator>
				<category><![CDATA[iCab]]></category>
		<category><![CDATA[WebKit]]></category>

		<guid isPermaLink="false">http://www.icab.de/blog/?p=9</guid>
		<description><![CDATA[Some iCab users asked me, if iCab 4 can use the latest WebKit nightly builds instead of the built-in WebKit component of Mac OS X. This is indeed possible, and thanks to an Automator Script by David Hall, this is also very easy to do. All you need are the following three files&#8230; a copy of iCab [...]]]></description>
			<content:encoded><![CDATA[<p>Some iCab users asked me, if iCab 4 can use the latest WebKit nightly builds instead of the built-in WebKit component of Mac OS X. This is indeed possible, and thanks to an Automator Script by <a href="http://spotthehall.com/" target="_blank">David Hall</a>, this is also very easy to do. All you need are the following three files&#8230;</p>
<ul>
<li>a copy of <a href="http://www.icab.de/dl.php" target="_blank"><strong>iCab 4</strong></a></li>
<li>the <a href="http://davidhall.me/running-on-webkit.html" target="_blank"><strong>Automator Script</strong></a> from David Hall</li>
<li>a binary copy of the <a href="http://webkit.org/" target="_blank"><strong>WebKit nightly</strong></a> builds</li>
</ul>
<p>Now copy the <strong>iCab 4</strong> application, the <strong>WebKit</strong> application and the Automator Script <strong>iCabWK</strong> into the Applications folder of your Mac. Make sure that neither <strong>iCab</strong> nor <strong>WebKit</strong> are running and double-click the Automator Script <strong>iCabWK</strong>. This will launch iCab so that it will use the WebKit framework that is located within the WebKit application.</p>
<p>To check if iCab is really using the WebKit nigthly build, you can download the <strong><a href="http://trac.webkit.org/wiki/DetectingWebKit" target="_blank">WebKitDetect</a></strong><a href="http://trac.webkit.org/wiki/DetectingWebKit" target="_blank"> script from the WebKit nightly web page</a> and open it in iCab. It will tell you which version of WebKit is currently used. But please make sure that you&#8217;ve configured the <strong>Identity</strong> setting of iCab (in the &#8220;View&#8221; menu) to the default value &#8220;<strong>Best compatibility</strong>&#8220;. The latter is important because changing the identity setting will change the value of the browsers &#8220;UserAgent&#8221; information, and the WebKit detection script is using the UserAgent information to determine the WebKit version that is used.</p>
<p>If you want to use the WebKit nightly within iCab, please always start iCab by double-clicking the iCabWK Automator Script. If you double-click iCab directly, or launch it indirectly by opening a HTML file or an URL, iCab will use the WebKit that is built-in into the system.</p>
]]></content:encoded>
			<wfw:commentRss>http://www.icab.de/blog/2009/07/26/icab-running-on-webkit-nightly/feed/</wfw:commentRss>
		<slash:comments>7</slash:comments>
		</item>
	</channel>
</rss>
