Chapter 11. Web Views

A web view is a web browser: it knows how to fetch resources from the Internet, and it understands and can render text coded in HTML, along with associated instructions coded as CSS and JavaScript. Thus it is a network communication device on the one hand, and a powerful engine for layout, animation, and media display on the other.

All of that power comes “for free” with a web view. It gives your app a browser interface, comparable to Mobile Safari; you can just stand back and let it do its work. You don’t have to know anything about networking. Links and other ancillary resources work automatically. If your web view’s HTML refers to an image, the web view will fetch it and display it. If the user taps on a link, the web view will fetch that content and display it; if the link is to some sort of media (a sound or video file), the web view will allow the user to play it.

A web view also knows how to display various other types of content commonly encountered as Internet resources. For example, a web view is an excellent way to display PDF files. It can also display documents in such formats as .rtf, Microsoft Word (.doc and .docx), and Pages. (A Pages file that is actually a bundle must be compressed to form a single .pages.zip resource.)

Warning

A web view should also be able to display .rtfd files, but in iOS 8 and 9, it can’t. In iOS 10, the file loads if it is zipped (type .rtfd.zip), but embedded images are not displayed, which makes the .rtfd more or less pointless. Apple suggests that you convert to an attributed string (as I described in Chapter 10; specify a document type of NSRTFDTextDocumentType), or use a QLPreviewController (Chapter 22).

The loading and rendering of a Web view’s content takes time, and may involve networking. Your app’s interface, however, is not blocked or frozen while the content is loading. On the contrary, your interface remains accessible and operative. The web view, in fetching a web page and its linked components, is doing something quite complex, involving both threading and network interaction — I’ll have a lot more to say about this in Chapters 23 and 24 — but it shields you from this complexity, and it operates asynchronously (in the background, off the main thread). Your own interaction with the web view stays on the main thread and is straightforward. You ask the web view to load some content; then you sit back and let it worry about the details.

Note

It is possible to design an entire app that is effectively nothing but a web view — 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 iOS apps that work this way; but such an approach to app design is outside the scope of this book.

There are actually three web view objects:

UIWebView

UIWebView, a UIView subclass, has been around since the earliest days of iOS. Apple would like you to move away from use of UIWebView, though as far as I can tell it has not yet been formally deprecated as of this writing.

WKWebView

WKWebView, a UIView subclass, appeared in iOS 8. The “WK” stands for WebKit; confusingly, both WKWebView and UIWebView use WebKit as their rendering engine, but WebKit is also the name of a framework that was introduced in iOS 8 as well. The arrival of the WebKit framework allows WKWebKit to perform some cool tricks that UIWebKit can’t do.

SFSafariViewController

SFSafariViewController, a UIViewController subclass, was introduced in iOS 9, as part of the Safari Services framework. It is a full-fledged browser, in effect identical to introducing Mobile Safari into your app, except that the initially loaded page is up to you, and the user cannot navigate to an arbitrary URL. It is, in effect, a form of Safari itself, running as a separate process within your app; in addition to built-in interface such as Forward and Back buttons and the Share button, it provides Safari features such as autofill and Reader, and shares its cookies with the real Safari app.

In this edition, I’ll describe WKWebView and SFSafariViewController. For a discussion of UIWebView, consult an earlier edition of this book.

Warning

iOS 9 introduced App Transport Security. Your app, by default, cannot load external URLs that are not secure (https:). You can turn off this restriction completely or in part in your Info.plist. See Chapter 23 for details.

WKWebView

WKWebView is part of the WebKit framework; to use it, you’ll need to import WebKit and create the web view in code, as there is no WKWebView in the nib editor’s Object library.

The designated initializer is init(frame:configuration:). The second parameter, configuration:, is a WKWebViewConfiguration. You can create a configuration beforehand:

let config = WKWebViewConfiguration()
// ... configure config here ...
let wv = WKWebView(frame: rect, configuration:config)

Alternatively, you can initialize your web view with init(frame:) to get a default configuration and modify it through the web view’s configuration property later:

let wv = WKWebView(frame: rect)
let config = wv.configuration
// ... configure config here ...

Either way, you’ll probably want to perform configurations before the web view has a chance to load any content, because some settings will affect how it loads or renders that content.

Here are some of the more important WKWebViewConfiguration properties:

suppressesIncrementalRendering

If true, the web view’s visible content doesn’t change until all linked renderable resources (such as images) have finished loading. The default is false.

allowsInlineMediaPlayback

If true, linked media are played inside the web page. The default is false (the fullscreen player is used).

mediaTypesRequiringUserActionForPlayback

Types of media that won’t start playing without a user gesture. A bitmask (WKAudiovisualMediaTypes) with possible values .audio, .video, and .all. New in iOS 10, superseding the more general requiresUserActionForMediaPlayback.

allowsPictureInPictureMediaPlayback

See Chapter 15 for a discussion of picture-in-picture playback.

dataDetectorTypes

Types of content that may be transformed automatically into tappable links. Similar to a text view’s data detectors (Chapter 10). New in iOS 10.

websiteDataStore

A WKWebsiteDataStore. By supplying a data store, you get control over stored resources. You can thus implement private browsing, examine and delete cookies, and so forth. For the types of data stored here, see the documentation on WKWebsiteDataRecord.

preferences

A WKPreferences object. This is a value class embodying three properties:

  • minimumFontSize

  • javaScriptEnabled

  • javaScriptCanOpenWindowsAutomatically

userContentController

A WKUserContentController object. This is how you can inject JavaScript into a web page and communicate between your code and the web page’s content. I’ll give an example later.

Having created your web view, you’ll place it in your interface and, if necessary, size and position it:

self.view.addSubview(wv)
wv.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
    NSLayoutConstraint.constraints(withVisualFormat:
        "H:|[wv]|", metrics: nil, views: ["wv":wv]),
    NSLayoutConstraint.constraints(withVisualFormat:
        "V:|[wv]|", metrics: nil, views: ["wv":wv])
].flatMap{$0})

A WKWebView is not a scroll view, but it has a scroll view (scrollView). You can use this to scroll the web view’s content programatically; you can also get references to its gesture recognizers, and add gesture recognizers of your own (see Chapter 7).

Web View Content

You can supply a web view with content using one of four methods, depending on the type of your content. All four methods return a WKNavigation object, an opaque object that can be used to identify an individual page-loading operation, but you will usually ignore it. The content types and methods are:

A URLRequest

Form a URLRequest and call load(_:). A URLRequest can be created from a URL. The initializer is init(url:cachePolicy:timeoutInterval:), but the second and third parameters are optional and will often be omitted. Additional configuration includes such properties as allowsCellularAccess. For example:

let url = URL(string: "https://www.apple.com")!
self.wv.load(URLRequest(url:url))
A local file

Obtain a local file URL and call loadFileURL(_:allowingReadAccessTo:). The second parameter effectively sandboxes the web view into a single file or directory. In this example from one of my apps, the HTML refers to images in the same directory as itself:

let url = Bundle.main.url(forResource: "zotzhelp", withExtension: "html")!
view.loadFileURL(url, allowingReadAccessTo: url)
An HTML string

Prepare a string consisting of valid HTML, and call loadHTMLString(_:baseURL:). The baseURL: specifies how partial URLs in your HTML are to be resolved; for example, the HTML might refer to resources in your app bundle.

Starting with an HTML string is useful particularly when you want to construct your HTML programmatically or make changes to it before handing it to the web view. In this example, my HTML consists of two strings: a wrapper with the usual <html> tags, and the body content derived from an RSS feed. I assemble them and hand the resulting string to my web view for display:

let templatepath = Bundle.main.path(
    forResource: "htmlTemplate", ofType:"txt")!
let base = URL(fileURLWithPath:templatepath)
var s = try! String(contentsOfFile:templatepath)
let ss = // actual body content for this page
s = s.replacingOccurrences(of:"<content>", with:ss)
self.wv.loadHTMLString(s, baseURL:base)
A Data object

Call load(_:MIMEType:characterEncodingName:baseURL:). This is useful particularly when the content has itself arrived from the network, as the parameters correspond to the properties of a URLResponse. This example will be more meaningful to you after you’ve read Chapter 23:

let sess = URLSession.shared
let url = URL(string:"https://www.someplace.net/someImage.jpg")!
let task = sess.dataTask(with: url) { data, response, err in
    if let response = response,
        let mime = response.mimeType,
        let enc = response.textEncodingName,
        let data = data {
            self.wv.load(data, mimeType: mime,
                characterEncodingName: enc, baseURL: url)
    }
}

Tracking Changes in a Web View

A WKWebView has properties that can be tracked with key–value observing, such as:

  • loading

  • estimatedProgress

  • url

  • title

You can observe these properties to be notified as a web page loads or changes. For example, as preparation to give the user feedback while a page is loading, I’ll put an activity indicator (Chapter 12) in the center of my web view, keep a reference to it, and observe the web view’s loading property:

let act = UIActivityIndicatorView(activityIndicatorStyle:.whiteLarge)
act.backgroundColor = UIColor(white:0.1, alpha:0.5)
self.activity = act
wv.addSubview(act)
act.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
    act.centerXAnchor.constraint(equalTo:wv.centerXAnchor),
    act.centerYAnchor.constraint(equalTo:wv.centerYAnchor)
])
wv.addObserver(self,
    forKeyPath: #keyPath(WKWebView.loading), options: .new, context: nil)

When the web view starts loading or stops loading, I’m notified, so I can show or hide the activity view:

override func observeValue(forKeyPath keyPath: String?,
    of object: Any?,
    change: [NSKeyValueChangeKey : Any]?,
    context: UnsafeMutableRawPointer?) {
        guard let wv = object as? WKWebView else {return}
        guard let keyPath = keyPath else {return}
        guard let change = change else {return}
        switch keyPath {
        case "loading":
            if let val = change[.newKey] as? Bool {
                if val {
                    self.activity.startAnimating()
                } else {
                    self.activity.stopAnimating()
                }
            }
        default:break
        }
}

Do not forget to remove yourself as an observer as you go out of existence. If, as is usually the case, this means also that the web view itself is going out of existence, I like to stop any loading that it may be doing at that moment as well:

deinit {
    self.wv.removeObserver(self, forKeyPath: #keyPath(WKWebView.loading))
    self.wv.stopLoading()
}

Web View Navigation

A WKWebView maintains a back and forward list of the URLs to which the user has navigated. The list is its backForwardList, a WKBackForwardList, which is a collection of read-only properties (and one method) such as:

  • currentItem

  • backItem

  • forwardItem

  • item(at:)

Each item in the list is a WKBackForwardItem, a simple value class basically consisting of a url and a title.

The WKWebView itself responds to goBack, goForward and go(to:), so you can tell it in code to navigate the list. Its properties canGoBack and canGoForward are key–value observable; typically you would use that fact to enable or disable a Back and Forward button in your interface in response to the list changing.

A WKWebView also has a settable property, allowsBackForwardNavigationGestures. The default is false; if true, the user can swipe sideways to go back and forward in the list.

To prevent or reroute navigation that the user tries to perform by tapping links, set yourself as the WKWebView’s navigationDelegate (WKNavigationDelegate) and implement webView(_:decidePolicyFor:decisionHandler:). You are handed a decisionHandler function which you must call, handing it a WKNavigationActionPolicy — either .cancel or .allow. You can examine the incoming navigationAction (the for: parameter, a WKNavigationAction) to help make your decision. It has a request which is the URLRequest we are proposing to perform — and you can look at its url to see where we are proposing to go — along with a navigationType which will be one of the following (WKNavigationType):

  • .linkActivated

  • .backForward

  • .reload

  • .formSubmitted

  • .formResubmitted

  • .other

In this example, I permit navigation in the most general case — otherwise nothing would ever appear in my web view! — but if the user taps a link, I forbid it and show that URL in Mobile Safari instead:

func webView(_ webView: WKWebView,
    decidePolicyFor navigationAction: WKNavigationAction,
    decisionHandler: @escaping (WKNavigationActionPolicy) -> Swift.Void) {
        if navigationAction.navigationType == .linkActivated {
            if let url = navigationAction.request.url {
                UIApplication.shared.open(url)
                decisionHandler(.cancel)
                return
            }
        }
        decisionHandler(.allow)
}

Several other WKNavigationDelegate methods can notify you as a page loads (or fails to load). Under normal circumstances, you’ll receive them in this order:

  • webView(_:didStartProvisionalNavigation:)

  • webView(_:didCommit:)

  • webView(_:didFinish:)

Those delegate methods, and all navigation commands, like the four ways of loading your web view with initial content, supply a WKNavigation object. This object is completely opaque and has no properties, but you can use it in an equality comparison to determine whether the navigations referred to in different methods are the same navigation (roughly speaking, the same page-loading operation).

Communicating with a Web Page

Your code can pass messages into and out of a WKWebView’s web page, thus allowing you to change the page’s contents or respond to changes within it, even while it is being displayed.

Communicating into a web page

To send a message into an already loaded WKWebView web page, call evaluateJavaScript(_:completionHandler:). Your JavaScript runs within the context of the web page.

In this example, the user is able to decrease the size of the text in the web page. We have prepared some JavaScript that generates a <style> element containing CSS that sets the font-size for the page’s <body> in accordance with a property, self.fontsize:

var fontsize = 18
var cssrule : String {
    var s = "var s = document.createElement('style');
"
    s += "s.textContent = '"
    s += "body { font-size: (self.fontsize)px; }"
    s += "';
"
    s += "document.documentElement.appendChild(s);
"
    return s
}

When the user taps a button, we decrement self.fontsize, construct that JavaScript, and send it to the web page:

func doDecreaseSize (_ sender: Any) {
    self.fontsize -= 1
    if self.fontsize < 10 {
        self.fontsize = 20
    }
    let s = self.cssrule
    self.wv.evaluateJavaScript(s)
}

That’s clever, but we have not done anything about setting the web page’s initial font-size. A WKWebView allows us to inject JavaScript into the web page at the time it is loaded. To do so, we use the userContentController of the WKWebView’s configuration. We create a WKUserScript, specifying the JavaScript it contains, along with an injectionTime which can be either before (.documentStart) or after (.documentEnd) a page’s content has loaded. In this case, we want it to be before; otherwise, the user will see the font size change suddenly:

let s = self.cssrule
let script = WKUserScript(source: s,
    injectionTime: .atDocumentStart, forMainFrameOnly: true)
let config = self.wv.configuration
config.userContentController.addUserScript(script)

Communicating out of a web page

To communicate out of a web page, you need first to install a message handler to receive the communication. Again, this involves the userContentController. You call add(_:name:), where the first argument is an object that must implement the WKScriptMessageHandler protocol, so that its userContentController(_:didReceive:) method can be called later:

let config = self.wv.configuration
config.userContentController.add(self, name: "playbutton")

We have now installed a playbutton message handler. This means that the DOM for our web page now contains an element, among its window.webkit.messageHandlers, called playbutton. A message handler sends its message when it receives a postMessage() function call. Thus, the WKScriptMessageHandler (self in this example) will get a call to its userContentController(_:didReceive:) method if JavaScript inside the web page sends postMessage() to the window.webkit.messageHandlers.playbutton object.

To make that actually happen, I’ve put an <img> tag into my web page’s HTML, specifying an image that will act as a tappable button:

<img src="listen.png"
 onclick="window.webkit.messageHandlers.playbutton.postMessage('play')">

When the user taps that image, the message is posted, and so my code runs and I can respond:

func userContentController(_ userContentController: WKUserContentController,
    didReceive message: WKScriptMessage) {
        if message.name == "playbutton" {
            if let body = message.body as? String {
                if body == "play" {
                    // ... do stuff here ...
                }
            }
        }
}

There’s just one little problem: that code causes a retain cycle. The reason is that a WKUserContentController leaks, and it retains the WKScriptMessageHandler, which in this case is self — and so self will never be deallocated. My solution is to create an intermediate trampoline object that can be harmlessly retained, and that has a weak reference to self:

class MyMessageHandler : NSObject, WKScriptMessageHandler {
    weak var delegate : WKScriptMessageHandler?
    init(delegate:WKScriptMessageHandler) {
        self.delegate = delegate
        super.init()
    }
    func userContentController(_ ucc: WKUserContentController,
        didReceive message: WKScriptMessage) {
            self.delegate?.userContentController(ucc, didReceive: message)
    }
}

Now when I add myself as a script message handler, I do it by way of the trampoline object:

let config = self.wv.configuration
let handler = MyMessageHandler(delegate:self)
config.userContentController.add(handler, name: "playbutton")

Now that I’ve broken the retain cycle, my own deinit is called, and I can release the offending objects:

deinit {
    self.wv.removeObserver(self, forKeyPath: #keyPath(WKWebView.loading))
    self.wv.stopLoading()
    // break all retains
    let ucc = self.wv.configuration.userContentController
    ucc.removeAllUserScripts()
    ucc.removeScriptMessageHandler(forName:"playbutton")
}

WKWebView Shortcomings

A WKWebView can’t be instantiated from a nib. The Web View object in the nib editor’s Object library is a UIWebView, not a WKWebView.

In iOS 8, a URL pointing to a resource on disk — including inside your app’s bundle — will fail to load with a WKWebView. This makes WKWebView unsuitable, say, for presentation of internal help documentation in iOS 8. The problem was fixed starting in iOS 9, but the fix isn’t backward compatible, so if your app runs on iOS 8 you might have to continue using UIWebView.

WKWebView, as far as I can tell, does not automatically participate in any way in the iOS view controller state saving and restoration mechanism. This is a major flaw in WKWebView. With UIWebView, if it has an actual URL request as the source of its content at the time the user leaves the app, then that URL request is archived by the state saving mechanism, along with the UIWebView’s Back and Forward lists and the content offset of its scroll view. If state restoration takes place, the UIWebView’s request property, and its Back and Forward lists, and its scroll view’s content offset, including the offsets of all previously viewed pages, are restored automatically; all you have to do is load the restored request, which you can easily do in applicationFinishedRestoringState, like this:

override func applicationFinishedRestoringState() {
    if self.wv.request != nil { // self.wv is a UIWebView
        self.wv.reload()
    }
}

But you can’t do anything like that with a WKWebView. It has no request property. It has a url property, but that property is not saved and restored. Moreover, a WKWebView’s backForwardList is not writable. Thus, there is no way to save and restore a WKWebView’s state as a web browser. This could be a reason for staying with UIWebView for now.

Safari View Controller

A Safari view controller (SFSafariViewController, introduced in iOS 9) puts the Mobile Safari interface in a separate process inside your app. It provides the user with a browser interface familiar from Mobile Safari itself. In a toolbar, which can be shown or hidden by scrolling, there are Back and Forward buttons, a Share button including standard Safari features such as Add Bookmark and Add to Reading List, and a Safari button that lets the user load the same page in the real Safari app. In a navigation bar, which can be shrunk or grown by scrolling, are a read-only URL field with a Reader button (if this page has a Reader mode available) and a Refresh button, and a Done button. The user has access to autofill and to Safari cookies with no intervention by your app.

The idea, according to Apple, is that when you want to present internal HTML content, such as an HTML string, you’ll use a WKWebView, but when you really want to allow the user to access the web, you’ll use a Safari view controller. In this way, you are saved from the trouble of trying to build a full-fledged web browser yourself.

To use a Safari view controller, you’ll need to import SafariServices. Create the SFSafariViewController, initialize it with a URL, and present it:

let svc = SFSafariViewController(url: url)
self.present(svc, animated: true)

The other chief configuration you can perform on an SFSafariViewController is to set the color of its navigation bar (preferredBarTintColor) and bar button items (preferredControlTintColor). This feature, new in iOS 10, should allow the look of the view to harmonize better with the rest of your app.

When the user taps the Done button, the Safari view controller is dismissed.

If you like, you can make yourself the Safari view controller’s delegate (SFSafariViewControllerDelegate) and implement any of these methods:

safariViewController(_:didCompleteInitialLoad:)
safariViewControllerDidFinish(_:)

Called on presentation and dismissal of the Safari view controller, respectively.

safariViewController(_:activityItemsFor:title:)

Allows you to supply your own Share button items; I’ll explain what activity items are in Chapter 13.

I have not found any way in which a Safari view controller participates in view controller state saving and restoration. Needless to say, I regard this as a bug.

Developing Web View Content

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 rendering engine. There are certain limitations; for example, mobile WebKit notoriously doesn’t use plug-ins, such as Flash, and it imposes limits on the size of resources (such as images) that it can display. On the plus side, WebKit is in the vanguard of the march toward HTML5 and CSS3, and has many special capabilities suited for display on a mobile device. For documentation and other resources, see Apple’s Safari Dev Center.

A good place to start is the Safari Web Content Guide. It contains links to 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 want to be aware of one important aspect of web page content — the viewport. This is typically set through a <meta> tag in the <head> area. For example:

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

Without that line, or something similar, a web page may be laid out incorrectly when it is rendered. Without a viewport, your content may appear tiny (because it is being rendered as if the screen were large), or it may be too wide for the view, forcing the user to scroll horizontally to read it. See the Safari Web Content Guide for details.

Tip

New in iOS 10, the viewport’s user-scalable attribute can be treated as yes by setting a WKWebViewConfiguration’s ignoresViewportScaleLimits to true.

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.

Inspecting, debugging, and experimenting with web view content is greatly eased by the Web Inspector, built into Safari on the desktop. It can see a web view in your app running in the Simulator, and lets you analyze every aspect of how it works. For example, in Figure 11-1, I’m examining an image to understand how it is sized and scaled.

pios 2401b
Figure 11-1. The Web Inspector inspects an app running in the Simulator

You can hover the mouse over a web page element in the Web Inspector to highlight the rendering of that element in the running app. Moreover, the Web Inspector lets you change your web view’s content in real time, with many helpful features such as CSS autocompletion.

JavaScript and the document object model (DOM) are also extremely powerful. Event listeners 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 iOS-native touchable views; it can also take advantage of Core Location and Core Motion facilities to respond to where the user is on earth and how the device is positioned (Chapter 21). Additional helpful documentation includes Apple’s WebKit DOM Programming Topics and Safari DOM Additions Reference.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.133.147.252