Nov 09

UIWebView has very few instance methods. One of them is stringByEvaluatingJavaScriptFromString, which very powerful and unfortunately poorly documented. (This is literally the extent of Apple’s explanation: “Returns the result of running a script.”)

Let’s explore this mysterious method with a couple of examples.

A trivial example of how to use stringByEvaluatingJavaScriptFromString is to get the title of the HTML document:

NSString *title = [webView stringByEvaluatingJavaScriptFromString:@"document.title"];

You would typically place this line of code in webViewDidFinishLoad.

This technique is not limited to one-liners, or accessing simple properties. Here’s an example of two lines of JavaScript code executed in order, as you would expect:

[webView stringByEvaluatingJavaScriptFromString:@"var field = document.getElementById('field_2');"
						 "field.value='Multiple statements - OK';"];

You can also call JavaScript functions this way. And if you want to call a JavaScript function that does not already exist in the web page that you’re downloading, you can “inject” it yourself with this technique:

[webView stringByEvaluatingJavaScriptFromString:@"var script = document.createElement('script');"
						 "script.type = 'text/javascript';"
						 "script.text = \"function myFunction() { "
							"var field = document.getElementById('field_3');"
							"field.value='Calling function - OK';"
						 "}\";"
						 "document.getElementsByTagName('head')[0].appendChild(script);"];

[webView stringByEvaluatingJavaScriptFromString:@"myFunction();"];

In essence I’m using Objective C to create a string which represents JavaScript which which when executed adds a JavaScript function to the HTML DOM. Apologies for the multiple meta levels… Let me try to untangle this line by line.

Line 1 : First we create a <script> element using JavaScript.
Line 2 : Set the type of the <script> element to text/javascript.
Line 3-6 : Set the content of the <script> element to the JavaScript function that you want to inject.
Line 7 : Add the new <script> element as a child to the <head> element of the HTML DOM.
Line 9 : Call the new JavaScript function.

Bonus tip: You can break up NSString constants over multiple lines in Xcode for increased readability. Just end the line with a double-quote character and begin the next line with a double-quote character. At compile time these lines will be joined into one string. So the string that begins with “var script” on Line 1 is one continuous string ending with “appendChild(script);” on Line 7.

Although it’s not critical for the discussion above, here’s the HTML that the example JavaScript refers to:

<html>
  <head>
    <meta name="viewport" content="width=320; initial-scale=1.0; maximum-scale=1.0"/>
  </head>
  <body>
    <p>This is the UIWebView</p>
    <form>
      <input id="field_1" type="text" name="value" /><br/>
      <input id="field_2" type="text" name="value" /><br/>
      <input id="field_3" type="text" name="value" /><br/>
    </form>
  </body>
</html>

You can use this generic technique to add JavaScript to any web page that you’re downloading and displaying in a UIWebView. The technique and the possibilities are similar to what you can do with the Greasemonkey plugin for Firefox.

written by Nick \\ tags: , ,

Oct 30

For a recent project I encountered the following requirements:

  1. Download and store HTML documents that are rendered using the UIWebView class.
  2. Download and store the external documents referenced in the HTML, like images, so that they, along with the HTML documents, are available in non-networked situations.
  3. Use the same HTML documents and external documents as the body of in-app generated Emails.

The challenge was to devise a scheme whereby the same HTML documents can be used for both rendering content within the app under non-networked situations and to provide the exact same user experience within the Email body; a clear Catch-22!

There are a number of possible approaches (see page 10 of Nick’s presentation, but the one we choose was to use complete URL’s for all externally referenced documents within the HTML (for example, all images are referenced on a CDN). We choose this route as it provides the simplest way to support the Email requirement.

Next we had to figure out a way to modify these URL’s dynamically to reference the same external documents located locally on the device. To do this we use some features of UIWebView and Javascript.

First we set up the UIWebView and implement the webViewDidFinishLoad delegate method to kick off the Javascript. Below you can see we take advantage of the webview’s stringByEvaluatingJavaScriptFromString() method to call our Javascript event listener useLocalPaths().

- (void)webViewDidFinishLoad:(UIWebView *)webView {
  DLog(@"Local URLs used = %@",[webView stringByEvaluatingJavaScriptFromString: @"window.useLocalPaths()"]);
}

The Javascript for the event listener is the following:

<script type="text/javascript">
  window.addEventListener('load',function() {
    window.useLocalPaths = function() {
      var images = document.getElementsByTagName('img');
      for (var i=0; i<images.length; i++) {
        var url = images[i].src;
        if (url.length>0) {
          var bits = url.split('/');
          images[i].src = bits[bits.length-1];
        }
      }
      return true;
    };
  },false);
</script>

In this case, this event listener queries for all image tags and modifies the src attribute by stripping all of the URL except for the image file name. For example if our image tag looked like this,

<img src="http://www.pervasentcdn.com/pervasent.jpg" class="logo" alt="Pervasent" />

it would be come this

<img src="pervasent.jpg" class="logo" alt="Pervasent" />

and thus achieves our goal.

It’s worth noting that the general approach of using UIWebView + Javascript is quite handy and obviously has many possible applications.

Update: If you’re having timing issues with this approach, you may want to take a look at this technique: How To Inject JavaScript Functions Into A UIWebView

written by Jess \\ tags: ,

Oct 02

This week I had the pleasure of both attending and speaking at the 360|iDev iPhone Developers Conference in Denver. Tom and John did an awesome job organizing this conference and they managed to gather an amazing group of speakers and attendees.

My own humble talk was titled UIWebView – The Most Versatile Class in UIKit. A topic that should be familiar to longtime readers of this blog. For the presentation I also took the time to create complete working code examples. Another favorite request around here…

Here are the links to download the presentation and the sample code.

written by Nick \\ tags:

Jul 07

I just got accepted to speak at the 360|iDev conference in Denver at the end of September. Unfortunately I missed the first 360|iDev conference in San Jose this past spring, but I heard that it was very good. Given the long list of distinguished speakers at the Denver conference, I’m fully expecting this one to be just as good. Check out the details here: 360idev.com

Note: The similarities between our company names, 360mind and 360iDev, and our location in Colorado, are purely coincidental.

The topic of my talk is UIWebView – The most versatile class in UIKit

Here are some of the tips and tricks you will learn in the session:

  • Load images, CSS and JavaScript from your app bundle.
  • Display more versatile result lists than UITableView.
  • Use dynamic CSS to resize text in response to gestures.
  • Receive touch events that are normally hidden by UIWebView.
  • Use JavaScript to interact with UIWebView content.

If you have other questions or subtopics related to UIWebView, please let me know in the comments below. I’ll try to work in as many as I can into the presentation.

written by Nick \\ tags: ,

Mar 03

Happy Square Root Day!

Playing a video (of a supported file format) on the iPhone is very easy using the MPMoviePlayerController class. You just create an instance of the class and initialize it with the URL of the video. The controller plays the video in full screen mode and returns back to your application when it’s done.

However, if the URL of the video is recognized by the iPhone as a YouTube URL then the Apple URL Scheme mechanism kicks in and launches the YouTube app. In this scenario control will not return to your app after the video has played. (The behavior is equivalent of calling [UIApplication openURL:] with the video URL.)

One workaround is to use a UIWebView and load it with the video URL. The drawback with this approach is that the user will see the rather ugly YouTube mobile web site and has to find and tap on the link of the video to play it.

YouTube Mobile

 

Another, visually more appealing, option is to create a small UIWebView on your screen and load it with the YouTube embed code. The result is a small button like image that shows the YouTube play button above a screen image from the video. When the user taps the image, the iPhone video player opens in full screen mode as usual, and when the video is done control is returned back to this screen.

YouTube Video Embedded in iPhone App

 

Here’s the code:

- (void)embedYouTube:(NSString*)url frame:(CGRect)frame {
 NSString* embedHTML = @"\
    <html><head>\
 <style type=\"text/css\">\
 body {\
 background-color: transparent;\
 color: white;\
 }\
 </style>\
 </head><body style=\"margin:0\">\
    <embed id=\"yt\" src=\"%@\" type=\"application/x-shockwave-flash\" \
 width=\"%0.0f\" height=\"%0.0f\"></embed>\
    </body></html>";
 NSString* html = [NSString stringWithFormat:embedHTML, url, frame.size.width, frame.size.height];
 if(videoView == nil) {
 	videoView = [[UIWebView alloc] initWithFrame:frame];
 	[self.view addSubview:videoView];
 }
 [videoView loadHTMLString:html baseURL:nil];
}

Tip of the hat to joehewitt.

Update: A complete working Xcode project has been posted here.

written by Nick \\ tags:

Feb 19

The iPhone media player is very easy to use, but it only works in landscape mode and it seems designed more for video than audio.  If you wish to play an onboard MP3 or an MP3 file as it’s downloading (not really streaming, but progressive download) you have another option!  Use the audio player embedded with Safari running in a UIWebView.

Here’s how to do it without having to create a special window for it.  Instantiate an UIWebView object using a 1×1 pixel sized frame (here self.playerView is an instance variable on my controller and is NOT added as a subview)

UIWebView *webView = [[UIWebView alloc] initWithFrame: CGRectMake(0.0, 0.0, 1.0, 1.0)];
webView.delegate = self;
self.playerView = webView;
[webView release];

Then load your NSURLRequest.

NSURLRequest *request = [[NSURLRequest alloc] initWithURL: [NSURL URLWithString: myMP3URL] cachePolicy: NSURLRequestUseProtocolCachePolicy timeoutInterval: myTimeoutValue];
[self.playerView loadRequest: request];
[request release];

That’s it!  When the request starts downloading the player with full controls will launch and your screen will look the the picture below.

Audio player launched from a UIWebView

Check out the web view delegate methods to learn more about controlling the behavior of the web view to suite your specific needs.

written by Jess \\ tags: ,

Dec 12

Among the most popular posts on this blog have been those about UIWebView. (The fact that it’s the #1 search result on Google for the term UIWebView might help. :)

Here’s I’d like to revisit the problem of displaying local images in a UIWebView. In this post I presented a little hack to embed small images into the HTML. While this may work in some cases, it’s far from an optimal solution. For example it does not address other types of external files you might want to refer to from your HTML.

Using relative paths or file: paths to refer to images does not work with UIWebView. Instead you have to load the HTML into the view with the correct baseURL: 

NSString *path = [[NSBundle mainBundle] bundlePath];
NSURL *baseURL = [NSURL fileURLWithPath:path];
[webView loadHTMLString:htmlString baseURL:baseURL];

You can then refer to your images like this:

<img src="myimage.png">

Or from within CSS like this:

background-image: url(loading.gif)

It’s important to note that images inside your application bundle are at the root of the bundle, even if you place them in an Images directory in your project.

 

written by Nick \\ tags: , ,

Apr 07

First you need to add your file to the Resources folder of your Xcode project. Then you can access the file like this (assuming the file is called MyFile.txt):

NSString *filePath = [[NSBundle mainBundle] pathForResource:@"MyFile" ofType:@"txt"];
NSData *myData = [NSData dataWithContentsOfFile:filePath];
if (myData) {
	// do something useful
}

Here’s a complete example reading a help text file into a UIWebView.

NSString *filePath = [[NSBundle mainBundle] pathForResource:@"HelpDoc" ofType:@"htm"];
NSData *htmlData = [NSData dataWithContentsOfFile:filePath];
if (htmlData) {
	[webView loadData:htmlData MIMEType:@"text/html" textEncodingName:@"UTF-8" baseURL:[NSURL URLWithString:@"http://iphoneincubator.com"]];
}

If you want to read the file into a string, which you can then display in a UITextView, for example, then do this:

NSString *filePath = [[NSBundle mainBundle] pathForResource:@"important" ofType:@"txt"];
if (filePath) {
	NSString *myText = [NSString stringWithContentsOfFile:filePath];
	if (myText) {
		textView.text= myText;
	}
}

written by Nick \\ tags: , , ,

Apr 03

A UIWebView is great for displaying rich text; you just format your text as HTML. But how do you display images in a UIWebView?

Since the iPhone is almost always connected you could just use a regular image tag referencing an image on a server somewhere:

<img src="http://example.com/img/foo.png" />

But that is less than optimal. What you really want to do is reference an image on the iPhone. One way to do this is to embed the image in the HTML using the data: URI scheme. Here’s an example:

<img src="data:image/png;base64,
iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAABGdBTUEAALGP
C/xhBQAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9YGARc5KB0XV+IA
AAAddEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIFRoZSBHSU1Q72QlbgAAAF1J
REFUGNO9zL0NglAAxPEfdLTs4BZM4DIO4C7OwQg2JoQ9LE1exdlYvBBeZ7jq
ch9//q1uH4TLzw4d6+ErXMMcXuHWxId3KOETnnXXV6MJpcq2MLaI97CER3N0
vr4MkhoXe0rZigAAAABJRU5ErkJggg==" alt="Red dot" />

Here’s a great tool for converting any small image to the base64 encoding required by data:image:
www.abluestar.com/utilities/encode_base64/index.php

Adding images this way will allow you to create great looking, self-contained help files and about us pages for your iPhone applications.

Update: See this post for an alternate way to display images: UIWebView – Loading External Images and CSS

written by Nick \\ tags: , ,

Mar 27

If you’re using a UIWebView to display rich text from a string of HTML (as I described yesterday) you can even include HTML links to external URLs.

However when the external web page is displayed it is done using the same scale as your local HTML page. Unless the two pages are about equal in size, the visual transition is less than pleasing.

You can use the scalesPageToFit property of UIWebView to fix this. See line 13:

- (void)loadView
{
  // Create a custom view hierarchy.
  CGRect appFrame = [[UIScreen mainScreen] applicationFrame];
  UIView *view = [[UIView alloc] initWithFrame:appFrame];
  view.autoresizingMask = UIViewAutoresizingFlexibleHeight|UIViewAutoresizingFlexibleWidth;
  self.view = view;
  [view release];

  CGRect webFrame = [[UIScreen mainScreen] applicationFrame];
  webView = [[UIWebView alloc] initWithFrame:webFrame];
  webView.backgroundColor = [UIColor whiteColor];
  webView.scalesPageToFit = YES;
  [self.view addSubview:webView];
}

But this causes another problem: your internal HTML page probably looks a lot smaller than you would like. To fix this we’ll use a trick that web app developers use to optimize web sites for the iPhone.

To the HTML code add a meta viewport tag:

<meta name="viewport" content="width=320"/>

The complete code looks like this:

NSString *html = @"<html><head><title>The Meaning of Life</title><meta name=\"viewport\" content=\"width=320\"/></head><body><p>...really is <b>42</b>!</p></body></html>";
[webView loadHTMLString:html baseURL:[NSURL URLWithString:@"http://www.hitchhiker.com/message"]];

written by Nick \\ tags: , ,