如果你开发一个应用程序,它是显示一个web页面或HTML文件,您可以使用WebKit框架,这是MacOS和iPhone
OS的一部分。
但是即使再mac上,webkit的框架也提供了奖金160个公共的头文件甚至有更多的类和方法,你可以使用他控制很多东西,包括加载,渲染显示和修改页面。但iPhone上仅仅给我们了一个类(UIWebView)来控制这一系列错作。尽管UIWebView用的是相同的WebKit组建,但mac是作为公有的API,而iPhone是作为私有的API,所以不能使用。很少的UIWebView的方法足够漂亮的文本,但是对于一个浏览器(iCab Moblie)或者其他基于网页的应用这是不够的,缺失很多的必要的方法。
一些例子:
UIWebView没有提供一个方法来获取当前显示的标题web页面,
它只是忽略所有试图打开链接,旨在打开新窗口或选项卡
它不允许访问HTML树
WebKit本身为所有这些任务提供了许多类,但他们都是私有的和iPhone所不具备的。
一些在应用商店的更改的浏览器都刚刚宣布这些限制作为一种特性(例如他们广告无法打开新窗口或标榜为“没有烦人的弹出式窗口”)。这听起来很不错,但现实使用中,这并不使这样的浏览器很有用。
所以我们能做些什么来克服这些局限性的UIWebView类?在Mac在iPhone没有违反与苹果iPhone
SDK协议下,我们可以(重新)实现所有很酷的特性是可用的WebKit框架,让iPhone和mac一样。不幸的是,我们不能。但我们可以实现许多丢失的特性。
如果你看一下可用的方法,只有一个,这将允许访问web页面的内容,这是或多或少的惟一办法回丢失的特性。和这种方法是
- (NSString *)stringByEvaluatingJavaScriptFromString:(NSString *)script;
这个方法是用来执行JavaScript代码上下文中的当前web页面,并返回一个字符串作为结果。这意味着我们必须使用JavaScript代码来实现我们需要的特性。
让我们先从一些非常容易。我们实现方法来获取标题和URL的当前显示的web页面。我们实现这个作为一个Objective-C“类别”,所以我们不需要子类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
我们现在在做什么呢?
从JavaScript的观点是,一个网页所代表的“文档”对象有几个属性。一个属性是“title”包含页面的标题。所以用“document.title”我们可以在JavaScript访问文档的标题。而这正是我们需要传递参数的方法”stringByEvaluatingJavaScriptFromString:“获得文档标题。
对于URL我们做类似的事情。
所以每当我们需要得到title或URL的web页面,显示在一个UIWebView对象,我们只需要调用“title”或“URL”方法:
NSString *title = [anyUIWebViewObject title];
接下来我们想做的限制可能是想地址是不能打开链接将打开一个新窗口。在Mac上的WebKit只会调用一个委托方法的主机应用程序请求一个新的WebView对象创建一个URL请求。应用程序将创建一个新的WebView对象并加载新页。但是在iPhone的UIWebView并不支持这样一个委托方法,因此所有试图打开一个链接只是忽略。
这些链接做通常看起来像这样:
<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.