As a courtesy, this is a full free rendering of my book, Programming iOS 6, by Matt Neuburg. Copyright 2013 Matt Neuburg. Please note that this edition is outdated; the current books are iOS 13 Programming Fundamentals with Swift and Programming iOS 13. If my work has been of help to you, please consider purchasing one or both of them, or you can reward me through PayPal at http://www.paypal.me/mattneub. Thank you!

Chapter 24. Web Views

A web view (UIWebView) is a UIView subclass that acts as a versatile renderer of text in various formats, including:

In addition to displaying rendered text, a web view is a web browser. This means that if you ask a web view to display HTML that refers to a resource available on disk or over the Internet, such as an image to be shown as the source of an <img> tag, the web view will attempt to fetch it and display it. Similarly, if the user taps, within the web view, on a link that leads to content on disk or over the Internet that the web view can render, the web view by default will attempt to fetch that content and display it. Indeed, a web view is, in effect, a front end for WebKit, the same rendering engine used by Mobile Safari (and by Safari on Mac OS X). A web view can display non-HTML file formats such as PDF, RTF, and so on, precisely because WebKit can display them.

As the user taps links and displays web pages, the web view keeps a Back list and a Forward list, just like a web browser. Two properties, canGoBack and canGoForward, and two methods, goBack and goForward, let you interact with this list. Your interface could thus contain Back and Forward buttons, like a miniature web browser.

A web view is scrollable, but UIWebView is not a UIScrollView subclass (Chapter 20); it has a scroll view, rather than being a scroll view. You can access a web view’s scroll view as its scrollView property. You can use the scroll view to learn and set how far the content is scrolled and zoomed, and you can install a gesture recognizer on it, to detect gestures not intended for the web view itself.

A web view is zoomable if its scalesToFit property is YES; in that case, it initially scales its content to fit, and the user can zoom the content (this includes use of the gesture, familiar from Mobile Safari, whereby double-tapping part of a web page zooms to that region of the page). Like a text view (Chapter 23), its dataDetectorTypes property lets you set certain types of data to be automatically converted to clickable links. An obvious difference from a text view is that the target of a web page link may be displayed right there in the web view, rather than switching to Mobile Safari.

It is possible to design an entire app that is effectively nothing but a UIWebView — especially if you have control of the server with which the user is interacting. Indeed, before the advent of iOS, an iPhone app was a web application. There are still iPhone apps that work this way, but such an approach to app design is outside the scope of this book. (See Apple’s Mobile Safari Web Application Tutorial if you’re curious.)

A web view’s most important task is to render HTML content; like any browser, a web view understands HTML, CSS, and JavaScript. In order to construct content for a web view, you must know HTML, CSS, and JavaScript. Discussion of those languages is beyond the scope of this book; each would require a book (at least) of its own.

Loading Web View Content

To load a web view with content initially, you’re going to need one of three things:

An NSURLRequest
Construct an NSURLRequest and call loadRequest:. An NSURLRequest might involve a file URL referring to a file on disk (within your app’s bundle, for instance); the web view will deduce the file’s type from its extension. But it might also involve the URL of a resource to be fetched across the Internet, in which case you can configure various additional aspects of the request (for example, you can form a POST request). This is the only form of loading that works with goBack (because in the other two forms, there is no URL to go back to).
An HTML string
Construct an NSString consisting of valid HTML and call loadHTMLString:baseURL:. The baseURL: will be used to fetch any resources referred to by a partial (relative) URL in the string. For example, you could cause partial URLs to refer to resources inside your app’s bundle.
Data and a MIME type
Obtain an NSData object and call loadData:MIMEType:textEncodingName:baseURL:. Obviously, this requires that you know the appropriate MIME type, and that you obtain the content as NSData (or convert it to NSData). Typically, this will be because the content was itself obtained by fetching it from the Internet (more about that in Chapter 37).

There is often more than one way to load a given piece of content. For instance, one of Apple’s own examples suggests that you display a PDF file in your app’s bundle by loading it as data, along these lines:

NSString *thePath =
    [[NSBundle mainBundle] pathForResource:@"MyPDF" ofType:@"pdf"];
NSData *pdfData = [NSData dataWithContentsOfFile:thePath];
[self.wv loadData:pdfData MIMEType:@"application/pdf"
                  textEncodingName:@"utf-8" baseURL:nil];

But the same thing can be done with a file URL and loadRequest:, like this:

NSURL* url =
    [[NSBundle mainBundle] URLForResource:@"MyPDF" withExtension:@"pdf"];
NSURLRequest* req = [[NSURLRequest alloc] initWithURL:url];
[self.wv loadRequest:req];

Similarly, in one of my apps, where the Help screen is a web view (Figure 24.1), the content is an HTML file along with some referenced image files, and I load it like this:

NSString* path =
    [[NSBundle mainBundle] pathForResource:@"help" ofType:@"html"];
NSURL* url = [NSURL fileURLWithPath:path];
NSError* err = nil;
NSString* s = [NSString stringWithContentsOfURL:url
                  encoding:NSUTF8StringEncoding error:&err];
// error-checking omitted
[view loadHTMLString:s baseURL:url];
figs/pios_2401.png

Figure 24.1. A Help screen that’s a web view


Observe that I supply both the string contents of the HTML file and the URL reference to the same file, the latter to act as a base URL so that the relative references to the images will work properly. (At the time I wrote that code, the NSBundle method URLForResource:withExtension: didn’t yet exist, so I had to form a pathname reference to the file and convert it to a URL.) In this instance, I could have used loadRequest: and the file URL:

NSString* path =
    [[NSBundle mainBundle] pathForResource:@"help" ofType:@"html"];
NSURL* url = [NSURL fileURLWithPath:path];
NSURLRequest* req = [[NSURLRequest alloc] initWithURL:url];
[view loadRequest: req];

You can use loadHTMLString:baseURL: to form your own web view content dynamically. For example, in the TidBITS News app, the content of an article is displayed in a web view that is loaded using loadHTMLString:baseURL:. The body of the article comes from an RSS feed, but it is wrapped in programmatically supplied material. Thus, in Figure 24.2, the title of the article and the fact that it is a link, the right-aligned author byline and publication date, and the Listen button, along with the overall formatting of the text (including the font size), are imposed as the web view appears.

figs/pios_2402.png

Figure 24.2. A web view with dynamically formed content


There are many possible strategies for doing this. In the case of the TidBITS News app, I start with a template loaded from disk:

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
"http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
  <meta http-equiv="content-type" content="text/html; charset=utf-8">
  <meta name="viewport" content="initial-scale=1.0, user-scalable=no">
  <!-- scale images down to fit -->
    <style type="text/css">
      p.inflow_image {
        text-align:center;
      }
      div.indented_image {
        text-align:center;
        margin-left:0;
      }
      img {
        max-width:<maximagewidth>;
        height:auto;
      }
    </style>
  <title>no title</title>
</head>
<body style="font-size:<fontsize>px; font-family:Georgia;
                                          margin:1px <margin>px">
  <!-- title, which is a link to original story at our site -->
  <div style="margin-top: 0px; margin-bottom: 15px">
    <h3><a href="<guid>"><ourtitle></a></h3>
  </div>
  <!-- playbutton or nothing; author and date -->
  <div style="width:100%%">
    <span style="float:left; display:block; vertical-align:middle">
        <playbutton>
    </span>
    <span style="float:right; margin-bottom: 15px;
                 display:block; text-align:right; font-size:80%%;">
      By <author><br><date>
    </span>
  </div>
  <!-- body, from feed -->
  <div style="clear:both; margin:30px 0px;">
    <content>
  </div>
</body>
</html>

The template defines the structure of a valid HTML document — the opening and closing tags, the head area (including some CSS styling), and a body consisting of <div>s laying out the parts of the page. But it also includes some tags that are not HTML, some of them appearing in impossible places — <maximagewidth>, <fontsize>, and so on. That’s because, when the web view is to be loaded, the template will be read from disk and real values will be substituted for those pseudo-tags:

NSString* template =
    [NSString stringWithContentsOfFile:
        [[NSBundle mainBundle] pathForResource:@"htmltemplate" ofType:@"txt"]
                              encoding: NSUTF8StringEncoding error:nil];
NSString* s = template;
s = [s stringByReplacingOccurrencesOfString:@"<maximagewidth>"
                                 withString:maxImageWidth];
s = [s stringByReplacingOccurrencesOfString:@"<fontsize>"
                                 withString:fontsize.stringValue];
s = [s stringByReplacingOccurrencesOfString:@"<margin>"
                                 withString:margin];
s = [s stringByReplacingOccurrencesOfString:@"<guid>"
                                 withString:anitem.guid];
s = [s stringByReplacingOccurrencesOfString:@"<ourtitle>"
                                 withString:anitem.title];
s = [s stringByReplacingOccurrencesOfString:@"<playbutton>"
                                 withString:(canPlay ? playbutton : @"")];
s = [s stringByReplacingOccurrencesOfString:@"<author>"
                                 withString:anitem.authorOfItem];
s = [s stringByReplacingOccurrencesOfString:@"<date>"
                                 withString:date];
s = [s stringByReplacingOccurrencesOfString:@"<content>"
                                 withString:anitem.content];

Some of these arguments (such as anitem.title, date, anitem.content) slot values more or less directly from the app’s model into the web view. Others are derived from the current circumstances. For example, the local variables maxImageWidth and margin have been set depending on whether the app is running on the iPhone or on the iPad; fontsize comes from the user defaults, because the user is allowed to determine how large the text should be. The result is an HTML string ready for loadHTMLString:baseURL:.

Web view content is loaded asynchronously (gradually, in a thread of its own), and it might not be loaded at all (because the user might not be connected to the Internet, the server might not respond properly, and so on). If you’re loading a resource directly from disk, loading is quick and nothing is going to go wrong; even then, though, rendering the content can take time, and even a resource loaded from disk, or content formed directly as an HTML string, might itself refer to material out on the Internet that takes time to fetch.

Your app’s interface is not blocked or frozen while the content is loading. On the contrary, it remains accessible and operative; that’s what “asynchronous” means. The web view, in fetching a web page and its linked components, is doing something quite complex, involving both threading and network interaction, but it shields you from this complexity. Your own interaction with the web view stays on the main thread and is straightforward. You ask the web view to load some content, and then you just sit back and let it worry about the details.

Indeed, there’s very little you can do once you’ve asked a web view to load content. Your main concerns will probably be to know when loading really starts, when it has finished, and whether it succeeded. To help you with this, a UIWebView’s delegate (adopting the UIWebViewDelegate protocol) gets three messages:

  • webViewDidStartLoad:
  • webViewDidFinishLoad:
  • webView:didFailLoadWithError:

In this example from the TidBITS News app, I mask the delay while the content loads by displaying in the center of the interface an activity indicator (a UIActivityIndicatorView, Chapter 25), referred to by a property, activity:

- (void)webViewDidStartLoad:(UIWebView *)wv {
    [self.view addSubview:self.activity];
    self.activity.center = CGPointMake(CGRectGetMidX(self.view.bounds),
                                       CGRectGetMidY(self.view.bounds));
    [self.activity startAnimating];
    [[UIApplication sharedApplication] beginIgnoringInteractionEvents];
}

- (void)webViewDidFinishLoad:(UIWebView *)webView {
    [self.activity stopAnimating];
    [self.activity removeFromSuperview];
    [[UIApplication sharedApplication] endIgnoringInteractionEvents];
}

- (void)webView:(UIWebView *)webView didFailLoadWithError:(NSError *)error {
    [self.activity stopAnimating];
    [self.activity removeFromSuperview];
    [[UIApplication sharedApplication] endIgnoringInteractionEvents];
}

Before designing the HTML to be displayed in a web view, you might want to read up on the brand of HTML native to the mobile WebKit engine. Of course a web view can display any valid HTML you throw at it, but the mobile WebKit has certain limitations. For example, mobile WebKit notoriously doesn’t use plug-ins, such as Flash; it doesn’t implement scrollable frames within framesets; and it imposes limits on the size of resources (such as images) that it can display. On the plus side, it has many special abilities and specifications that you’ll want to take advantage of; for example, WebKit is in the forefront of the march towards HTML 5.

A good place to start is Apple’s Safari Web Content Guide. It contains links to all the other relevant documentation, such as the Safari CSS Visual Effects Guide, which describes some things you can do with WebKit’s implementation of CSS3 (like animations), and the Safari HTML5 Audio and Video Guide, which describes WebKit’s audio and video player support.

If nothing else, you’ll definitely want to be aware of one important aspect of web page content — the viewport. You’ll notice that the TidBITS News HTML template I showed a moment ago contains this line within its <head> area:

<meta name="viewport" content="initial-scale=1.0, user-scalable=no">

Without that line, the HTML string is laid out incorrectly when it is rendered. This is noticeable especially with the iPad version of TidBITS News, where the web view can be rotated when the device is rotated, causing its width to change: in one orientation or the other, the text will be too wide for the web view, and the user has to scroll horizontally to read it. The Safari Web Content Guide explains why: if no viewport is specified, the viewport can change when the app rotates. Setting the initial-scale causes the viewport size to adopt correct values in both orientations.

Another important section of the Safari Web Content Guide describes how you can use a media attribute in the <link> tag that loads your CSS to load different CSS depending on what kind of device your app is running on. For example, you might have one CSS file that lays out your web view’s content on an iPhone, and another that lays it out on an iPad.

A web view’s loading property tells you whether it is in the process of loading a request. If, at the time a web view is to be destroyed, its loading is YES, it is up to you to cancel the request by sending it the stopLoading message first; actually, it does no harm to send the web view stopLoading in any case. In addition, UIWebView is one of those weird classes I warned you about (in Chapter 12) whose memory management behavior is odd: Apple’s documentation warns that if you assign a UIWebView a delegate, you must nilify its delegate property before releasing the web view. Thus, in a controller class whose view contains a web view, I do an extra little dance in dealloc:

- (void) dealloc {
    [self.wv stopLoading];
    self.wv.delegate = nil;
}

A related problem is that a web view will sometimes leak memory. I’ve never understood what causes this, but the workaround appears to be to load the empty string into the web view. The TidBITS News app does this in the view controller whose view contains the web view:

- (void) viewWillDisappear:(BOOL)animated {
    if (self.isMovingFromParentViewController) {
        [self.wv loadHTMLString: @"" baseURL: nil];
    }
}

The suppressesIncrementalRendering property, new in iOS 6, changes nothing about the request-loading process, but it does change what the user sees. The default, and the old standard behavior, is NO: the web view assembles its display of a resource incrementally, as it arrives. If this property is YES, the web view does nothing outwardly until the resource has completely arrived and the web view is ready to render the whole thing.

Web View State Restoration

If you provided an HTML string to your web view, then restoring its state when the app is relaunched is up to you. You can use the built-in iOS 6 state saving and restoration to help you, but you’ll have to do all the work yourself. The web view has a scrollView which has a contentOffset, so it’s easy to save the scroll position (as an NSValue wrapping a CGPoint) in encodeRestorableStateWithCoder:, and restore it in decodeRestorableStateWithCoder:. What the TidBITS News app does is to restore the scroll position initially into an instance variable:

-(void)decodeRestorableStateWithCoder:(NSCoder *)coder {
    // scroll position is a CGPoint wrapped in an NSValue
    self.lastOffset = [coder decodeObjectForKey:@"lastOffset"];
    // ... other stuff ...
    [super decodeRestorableStateWithCoder:coder];
}

Then we reload the web view content (manually); when the web view has loaded, we set its scroll position:

- (void)webViewDidFinishLoad:(UIWebView *)webView {
    if (self.lastOffset)
        webView.scrollView.contentOffset = self.lastOffset.CGPointValue;
    self.lastOffset = nil;
    // ...
}

If, however, a web view participates in state restoration, and if the web view had a URL request (not an HTML string) when the user left the app, the web view will automatically return to life containing that request in its request property, and with its Back and Forward lists intact. Thus, you can use the state restoration mechanism to restore the state of the web view, but you have to perform a little extra dance. This dance is so curious and obscure that initially I was under the impression that a web view’s state couldn’t really be saved and restored, despite the documentation’s assertion that it could.

There are two secrets here; once you know them, you’ll understand web view state restoration:

  • A restored web view will not automatically load its request; that’s up to your code.
  • After a restored web view has loaded its request, the first item in its Back list is the same page in the state the user left it (scroll and zoom).

Knowing this, you can easily devise a strategy for web view state restoration. The first thing is to detect that we are restoring state, and raise a flag that says so:

-(void)decodeRestorableStateWithCoder:(NSCoder *)coder {
    [super decodeRestorableStateWithCoder:coder];
    self->_didDecode = YES;
}

Now we can detect (perhaps in viewDidAppear:) that we are restoring state, and that the web view magically contains a request, and load that request:

if (self->_didDecode && wv.request)
    [wv loadRequest:wv.request];

Now for the tricky part. After the view loads, we immediately “go back.” This actually has the effect of restoring the user’s previous scroll position (and of removing the extra entry from the top of the Back stack). Then we lower our flag so that we don’t make this extra move at any other time:

- (void)webViewDidFinishLoad:(UIWebView *)wv {
    if (self->_didDecode && wv.canGoBack)
        [wv goBack];
    self->_didDecode = NO;
}

Communicating with a Web View

Having loaded a web view with content, you don’t so much configure or command the web view as communicate with it. There are two modes of communication with a web view and its content:

Load requests

When a web view is asked to load content, possibly because the user has tapped a link within it, its delegate is sent the message webView:shouldStartLoadWithRequest:navigationType:. This is your opportunity to interfere with the web view’s loading behavior; if you return NO, the content won’t load.

The second argument is an NSURLRequest, whose URL property you can analyze (very easily, because it’s an NSURL). The third argument is a constant describing the type of navigation involved, whose value will be one of the following:

  • UIWebViewNavigationTypeLinkClicked
  • UIWebViewNavigationTypeFormSubmitted
  • UIWebViewNavigationTypeBackForward
  • UIWebViewNavigationTypeReload
  • UIWebViewNavigationTypeFormResubmitted
  • UIWebViewNavigationTypeOther (includes loading the web view with content initially)
JavaScript execution
You can speak JavaScript to a web view’s content by sending it the stringByEvaluatingJavaScriptFromString: message. Thus you can enquire as to the nature and details of that content, and you can alter the content dynamically.

The TidBITS News app uses webView:shouldStartLoadWithRequest:navigationType: to distinguish between the user tapping an ordinary link and tapping the Listen button (shown in Figure 24.2). The onclick script for the <a> tag surrounding the Listen button image executes this JavaScript code:

document.location='play:me'

This causes the web view to attempt to load an NSURLRequest whose URL is play:me, which is totally bogus; it’s merely an internal signal to ourselves. In the web view’s delegate, we intercept the attempt to load this request, examine the NSURLRequest, observe that its URL has a scheme called @"play", and prevent the loading from taking place; instead, we head back to the Internet to start playing the online podcast recording associated with this article. Any other load request caused by tapping a link is also prevented and redirected instead to Mobile Safari, because we don’t want our web view used as an all-purpose browser. But we do let our web view load a request in the general case, because otherwise it wouldn’t even respond to our attempt to load it with HTML content in the first place:

- (BOOL)webView:(UIWebView *)webView
        shouldStartLoadWithRequest:(NSURLRequest *)r
        navigationType:(UIWebViewNavigationType)nt {
    if ([r.URL.scheme isEqualToString: @"play"]) {
        [self doPlay:nil];
        return NO;
    }
    if (nt == UIWebViewNavigationTypeLinkClicked) {
        [[UIApplication sharedApplication] openURL:r.URL];
        return NO;
    }
    return YES;
}

JavaScript and the document object model (DOM) are quite powerful. Event listeners even allow JavaScript code to respond directly to touch and gesture events, so that the user can interact with elements of a web page much as if they were touchable views; it can also take advantage of Core Location facilities to respond to where the user is on earth and how the device is positioned (Chapter 35).

Additional helpful documentation includes Apple’s WebKit DOM Programming Topics, WebKit DOM Reference, and Safari DOM Additions Reference.