Jan 20

Yesterday Apple introduced iBooks Author at an education event in New York City. Almost immediately people were up in arms: How dare Apple introduce an application for free that does not meet all of their own personal needs? And the audacity of insisting on a EULA that favors Apple’s own devices and markets. Not to mention the fact that iBooks Author only runs on Macs. Of course I would also prefer a tool that solves more of my issues related to publishing, along with unicorns and free beer. But, since I’m not footing the bill for the development of iBooks Author, my opinions on this don’t carry much weight. So let’s move on and look at what iBooks Author really is.

iBooks Author is intended to solve a very narrowly defined problem: How to easily create and publish great looking interactive eBooks for the iBookstore to be read on iPads. From what I’ve seen, it solves this problem very well.

iBooks Author is not intended to be a general purpose ePub authoring tool. This is of course disappointing. Most people don’t know that Apple already has one of the very few commercial, easy to use tools for creating ePub files in the iWork Pages application for the Mac. It’s not all that great, but Pages solves the problem of writing a book containing mostly text, adding some images and saving it as an ePub. A disappointing consequence of the introduction of iBooks Author is probably that Pages will not see many future improvements to it’s ePub capabilities.

.ibooks vs. ePub

The output from iBooks Author is like ePub but not quite. If you open up the .ibooks file (it’s a zip archive just like ePub files are) you will see the same structure as an ePub. Each page has it’s own xhtml file, and there are a couple of XML files that describe the structure of the book.

Creating an ePub reader for iOS is not terribly difficult since the individual pages or chapters are pretty standard HTML files which can be displayed in a UIWebView. I believe that iBooks v1.x in essence was built around a UIWebView. But that has changed significantly with iBooks 2.

Examining xhtml files generated by iBooks Author in more detail and you’ll find a lot of non-standard CSS: -ibooks-layout-hint:, -ibooks-list-text-indent:, -ibooks-strikethru-type:, etc. I know there are several WebKit specific CSS extensions with the prefix -webkit, but I don’t know what WebKit, and much less a UIWebView would do with these new -ibooks extensions.

But it gets worse.

When you use one of the built-in widgets you will get code like this:
<object type=”application/x-ibooks+anchored” data-anchor-ref=”danchor-gallery-0″> This code must trigger something in the iBooks 2 binary that renders the widget. It would have been nicer if iBooks Author generated standard JavaScript+CSS here instead. The native approach was probably done for performance reasons, as anyone who has tried to develop complex, interactive animations with JavaScript and CSS can attest to.

A consequence of these design decisions is that it’s no longer possible to load the xhtml files into a UIWebView and see a fully rendered page. This begs the question of how did they implement these new features in iBooks 2? I don’t see any way to just use the public APIs of a UIWebView to accomplish this. But given the debacle when iBooks was caught using a private API for adjusting the screen brightness, it seems unwise to go down that path again. Maybe they started from scratch using the WebKit open source code? They would certainly have the talent at Apple to pull that off.

So why did Apple choose proprietary extensions to ePub instead of fully conforming to the standard? We can only speculate.

  • They want to lock in publishers to the iBookstore to boost the lagging number of titles.
  • They want to ensure only iPads sales benefit from this free tool.
  • They want full control over the format themselves.
  • Time and budget constraints forced them to take the easier path.
  • The current ePub standard does not provide all the functions and features they wanted to include.

I think there is some truth in all of the above, but I would put my money on the last one. See also John Gruber’s insightful analysis.

Given that the output format is closed, it would be possible for Apple to switch to a fully ePub compliant format as soon as the standards catch up. I don’t think this is very likely. But keep in mind that this is just the first version of iBooks Author.

Lack of iPhone compatibility

Curiously iBooks Author only generates ebooks for the iPad. I don’t think there are any technical reasons for excluding the iPhone and iPod touch. It’s more likely to be a content production issue. Creating the page layouts for these enhanced ebooks for the iPad is expensive enough. Forcing publishers to do the work twice to also target the iPhone, would probably be too much to ask.

For the educational market, the iPad is probably the right target. But if you are contemplating using iBooks Author to create ebooks for a wider audience, then the lack of support for iPhone and iPod touch is definitely a factor to consider.

Newsstand

One seemingly obvious application for iBooks Author is to create magazines to be distributed via Newsstand. Jason Snell of MacWorld wishes for this. The current EULA forbids content created with iBooks Author to be distributed via Newsstand (unless you give it away for free), but that is something it shouldn’t be too difficult to get an exception from Apple for, or conceivably see Apple change the EULA to include Newsstand in addition to iBookstore.

But given the architecture of iBooks 2 and the files generated by iBooks Author, it will be a challenge for third party developers to create magazine reader apps to display content generated by iBooks Author.

So should Apple create a generic magazine reader app, e.g. iNewsstand, that has similar features as iBooks, but also handles subscriptions and delivering content using the Newsstand infrastructure? That would be a bit odd to introduce now, as it would become a newsstand within the Newsstand folder. (Apple has actively discouraged third party magazine aggregators from making their apps Newsstand enabled.)

How about a sample project that magazine publishers can download and expand for their own use? This also seems unlikely as that would diminish some of the benefits Apple is achieving (deliberately or as a side effect) from the new proprietary file format.

IBooksPageViewController

An elegant solution to the problem of giving third party developers the ability to display content generated by iBooks Author would be to provide a new view controller that can read and display .ibooks files. It would be like a UIWebView on steroids. We can debate wether it should be a view or a view controller. Given the fixed page layout nature of ebooks created in iBooks Author, I think a view controller would be more appropriate.

Imagine the cool apps that can be created by developers if the creativity unleashed by the free iBooks Author tool could be harnessed in iOS apps. If Apple’s true intention is to keep the file formats proprietary and the technologies exclusive to the iOS platform, that is still achieved and further promoted with this solution.

I like this idea. I think I’ll go right now and submit a Radar for it.

written by Nick \\ tags: , ,

Oct 28

Apple has made several subtle changes related to case (as in upper/lower/mixed-case) in the URL and HTTP communication classes for iOS 5. Here’s the first one, and more blog posts to follow.

With a UIWebView I often implement the shouldStartLoadWithRequest method in the UIWebViewDelegate to look at the link the user tapped and take specific action for certain types of links. Here’s a simple example that handles mailto links:

- (BOOL)webView:(UIWebView *)theWebView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType {
	BOOL shouldStartLoadWithRequest = YES;

	NSString *scheme = [[request URL] scheme];
	if ([scheme isEqualToString:@"mailto"]) {
		[[UIApplication sharedApplication] openURL:[request URL]];
		shouldStartLoadWithRequest = NO;
	}

	return shouldStartLoadWithRequest;
}

Another common use is to handle application specific links like “MyApp://goto?id=42″. If you just replaced @”mailto” with @”MyApp” in the example code above, it turns out that this breaks in iOS 5. The reason is that iOS 5 returns “myapp” (all lowercase) for the scheme, whereas iOS 4 and earlier returned the actual string from the HTML link unaltered.

The solution is simple. Just make your string comparison case insensitive. For example:

if ([scheme compare:@"MyApp" options:NSCaseInsensitiveSearch] == NSOrderedSame) {

written by Nick \\ tags: , ,

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: , , ,