Chapter 12. Web Views

This chapter has been revised for Early Release. It reflects iOS 14, Xcode 12, and Swift 5.3. But screenshots have not been retaken; they still show the Xcode 11 / iOS 13 interface.

A web view is a web browser, which is a powerful thing: it knows how to fetch resources through the internet, and it can render HTML and CSS, and can respond to JavaScript. It is a network communication device, as well as an interactive layout, animation, and media display engine.

In a web view, 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 can also display some other types of content commonly encountered as internet resources. It can display PDF files, as well as 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 this feature is not working properly; Apple suggests that you convert to an attributed string as I described in Chapter 11 (specifying a document type of NSRTFDTextDocumentType), or use a QLPreviewController (Chapter 23).

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 and rendering 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 24 and 25 — but it shields you from this complexity, and it operates 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.

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 24 for details.

WKWebView

WKWebView is part of the WebKit framework (import WebKit). 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 modify the web view’s configuration later, through its configuration property. 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 automatically, without a user gesture. A bitmask (WKAudiovisualMediaTypes) with possible values .audio, .video, and .all.

allowsPictureInPictureMediaPlayback

See Chapter 16 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 11).

websiteDataStore

A WKWebsiteDataStore. By supplying a data store, you get control over stored resources. Its httpCookieStore is a WKHTTPCookieStore where you can examine, add, and remove cookies.

preferences

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

  • minimumFontSize

  • javaScriptCanOpenWindowsAutomatically

  • isFraudulentWebsiteWarningEnabled

(New in iOS 14, the javaScriptEnabled property is deprecated; JavaScript enablement is now moved to the WKWebpagePreferences class, as I’ll explain later.)

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. Also, you can give the userContentController a rule list (WKContentRuleList) that filters the web view’s content.

limitsNavigationsToAppBoundDomains

New in iOS 14. If true, the user will be unable to navigate to any domains other than those listed in your Info.plist under the key WKAppBoundDomains, whose value is an array of strings. The default is false.

(Even if this property is false, the WKAppBoundDomains value has another purpose: the listed domains are the only ones for which JavaScript injection, custom style sheets, cookie manipulation, and message handlers are enabled. Turning off those features effectively wards off malicious user tracking. An empty array turns them off for all domains. If limitsNavigationsToAppBoundDomains is true, those features are not turned off for domains listed in the WKAppBoundDomains array.)

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 programmatically and to respond as the scroll view’s delegate when the user scrolls, plus you can get references to the scroll view’s gesture recognizers and add gesture recognizers of your own.

You can take a snapshot of a web view’s content by calling takeSnapshot(with:completionHandler:). The snapshot image is passed into the completion function as a UIImage. New in iOS 14, you can snapshot the content as a PDF with createPDF(configuration:completionHandler:), and you can export the content as a web archive with createWebArchiveData(completionHandler:).

In the nib editor, the Objects library contains a WKWebView object, referred to as a WebKit View, that you can drag into your interface as you design it; you might need to link to the WebKit framework manually (in the app target’s Link Binary With Libraries build phase) to prevent the app from crashing as the nib loads. Many WKWebViewConfiguration and WKPreferences properties can be configured in the nib editor as well.

Web View Content

You can supply a web view with content using one of four methods, depending on the content’s type:

A URLRequest

Form a URLRequest from a URL and call load(_:). The URLRequest initializer is init(url:cachePolicy:timeoutInterval:), but the second and third parameters are optional and will often be omitted. Additional URLRequest configuration includes such properties as allowsExpensiveNetworkAccess (see Chapter 24):

let url = URL(string: "https://www.apple.com")!
let req = URLRequest(url: url)
// could set req.allowsExpensiveNetworkAccess here
self.wv.load(req)
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 file zotzhelp.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 (as when the HTML refers 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 from the TidBITS News app, 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 24:

let sess = URLSession.shared
let url = // whatever
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)
    }
}

All four methods return a WKNavigation object, but I ignored it in my examples. If you like, you can capture it to identify an individual page-loading operation, as I’ll explain later.

Tracking Changes in a Web View

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

  • isLoading

  • estimatedProgress

  • url

  • title

You can observe these properties to be notified as a web page loads or changes.

To illustrate, I’ll give the user feedback while a page is loading by displaying an activity indicator (Chapter 13). I’ll start by putting the activity indicator in the center of my web view and keeping a reference to it:

let act = UIActivityIndicatorView(style:.large)
act.backgroundColor = UIColor(white:0.1, alpha:0.5)
act.color = .white
self.wv.addSubview(act)
act.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
    act.centerXAnchor.constraint(equalTo:wv.centerXAnchor),
    act.centerYAnchor.constraint(equalTo:wv.centerYAnchor)
])
self.activity = act

Now I observe the web view’s isLoading property (self.obs is a Set instance property). When the web view starts loading or stops loading, I’m notified, so I can show or hide the activity view:

let ob = self.wv.observe(.isLoading, options:.new) {[unowned self] wv,ch in
    if let val = ch.newValue {
        if val {
            self.activity.startAnimating()
        } else {
            self.activity.stopAnimating()
        }
    }
}
self.obs.insert(ob)

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

  • 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.

A WKWebView 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. This property can also be set in the nib editor.

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:preferences:decisionHandler:). The for: parameter is a WKNavigationAction that you can examine to help make your decision. Among other things, its request property is the URLRequest we are proposing to perform — 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

You are also handed a decisionHandler function which you must call with a WKNavigationActionPolicy argument — either .cancel or .allow. You must also pass a WKWebpagePreferences object in your call to this function; I’ll talk in a moment about what it’s for, but in general you can just pass the preferences object that arrived as a parameter.

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,
    preferences: WKWebpagePreferences, decisionHandler:
    @escaping (WKNavigationActionPolicy, WKWebpagePreferences) -> Void) {
        if navigationAction.navigationType == .linkActivated {
            if let url = navigationAction.request.url {
                UIApplication.shared.open(url)
                decisionHandler(.cancel, preferences) // *
                return
            }
        }
        decisionHandler(.allow)
}

Starting in iOS 13, a web view has a content mode (WKWebpagePreferences.ContentMode), determining how the web view represents itself as a browser to the server:

.desktop

The default on an iPad when the web view is fullscreen. Web sites will appear in their desktop version.

.mobile

The default on an iPhone, or on the iPad when the web view is not fullscreen (perhaps it’s in a popover, or we’re doing iPad multitasking). Web sites will appear in their mobile version.

To access desktop mode in a WKWebView, you’ll need to set the configuration’s applicationNameForUserAgent property to a desktop browser’s user agent string, such as "Version/13.1.2 Safari/605.1.15". You can set this in the nib editor.

To change the content mode for a particular web page during navigation, use your implementation of webView(_:decidePolicyFor:preferences:decisionHandler:). The third parameter, preferences, is a WKWebpagePreferences object, a simple value class. Set its preferredContentMode to .desktop or .mobile, and pass the revised object into the decisionHandler call as the second argument.

New in iOS 14, another use of the preferences object is to enable or disable JavaScript; set its allowsContentJavaScript as desired. This toggles JavaScript on a per-page basis. It applies, as the name suggests, to JavaScript that comes from the loaded content; JavaScript that you inject into the page from your code, as I’ll describe in the next section, is still effective even if this property is set to false.

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, including the four ways of loading your web view with initial content, supply a WKNavigation object. You can use this 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). This object also has an effectiveContentMode property that tells you the current content mode (.desktop or .mobile).

Communicating with a Web Page

Your code can pass JavaScript messages into and out of a WKWebView’s web page, 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(_:in:in:completionHandler:). This method is new in iOS 14, superseding evaluateJavaScript(_:completionHandler:). With the new method, not only can you specify frame information as the context in which your JavaScript should run, but also you specify a WKContentWorld, which will usually be .defaultClient; this keeps your JavaScript and the content’s JavaScript from impinging on one another, accidentally or maliciously.

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 {
    return """
    var s = document.createElement('style');
    s.textContent = 'body { font-size: (self.fontsize)px; }';
    document.documentElement.appendChild(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)
}

But there’s something rather inelegant about that. Our cssrule getter is performing string interpolation in order to assemble the JavaScript. That’s ugly, and it would be a lot uglier if there were additional interpolations. New in iOS 14, there’s an elegant solution. Instead of evaluateJavaScript(_:in:in:completionHandler:), you can call callAsyncJavaScript(_:arguments:in:in:completionHandler:). This lets you pass an arguments: dictionary of substitutions to be performed for you! To demonstrate, I’ll construct our JavaScript a different way:

var cssrule2 : String {
    return """
    var s = document.createElement('style');
    s.textContent = `body { font-size: ${thefontsize}px; }`;
    document.documentElement.appendChild(s);
    """
}

That JavaScript uses JavaScript string interpolation (a template literal). There’s no variable called thefontsize; that’s what we have to supply as an argument:

self.wv.callAsyncJavaScript(self.cssrule2,
    arguments: ["thefontsize": self.fontsize],
    in: nil, in: .defaultClient)

Finally, we have not done anything about setting the web page’s initial font-size. Let’s fix that.

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 script = WKUserScript(source: self.cssrule,
    injectionTime: .atDocumentStart, forMainFrameOnly: true,
    in: .defaultClient)
let config = self.wv.configuration
config.userContentController.addUserScript(script)

(It’s a pity that we can’t supply substitution arguments in that call.)

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(_:contentWorld:name:). Again, this method is new in iOS 14, replacing add(_:name:) so that you can specify a content world for the JavaScript to run in. 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,
    contentWorld: .defaultClient, name: "playbutton")

We have just 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. 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, defining 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. But self is the view controller, so that’s very bad. 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 {
    let ucc = self.wv.configuration.userContentController
    ucc.removeAllUserScripts()
    ucc.removeScriptMessageHandler(forName:"playbutton")
}

New in iOS 14, if you need to receive a message from the web page and reply to it, you can. Instead of calling add(_:contentWorld:name:), you call addScriptMessageHandler(_:contentWorld:name:). Instead of conforming to WKScriptMessageHandler, the first parameter conforms to WKScriptMessageHandlerWithReply, meaning that it implements userContentController(_:didReceive:replyHandler:). The JavaScript sends a Promise, so your reply can be asynchronous.

JavaScript alerts

If a web page tries to put up a JavaScript alert, nothing will happen in your app unless you assign the WKWebView a uiDelegate, an object adopting the WKUIDelegate protocol, and implement these methods:

webView(_:runJavaScriptAlertPanelWithMessage:initiatedByFrame:completionHandler:)

Called by JavaScript alert.

webView(_:runJavaScriptConfirmPanelWithMessage:initiatedByFrame:​completionHandler:)

Called by JavaScript confirm.

webView(_:runJavaScriptTextInputPanelWithPrompt:defaultText:initiatedByFrame:completionHandler:)

Called by JavaScript prompt.

Your implementation should put up an appropriate alert (UIAlertController, see Chapter 14) and call the completion function when it is dismissed. Here’s a minimal implementation for the alert method:

func webView(_ webView: WKWebView,
    runJavaScriptAlertPanelWithMessage message: String,
    initiatedByFrame frame: WKFrameInfo,
    completionHandler: @escaping () -> Void) {
        let host = frame.request.url?.host
        let alert = UIAlertController(title: host, message: message,
            preferredStyle: .alert)
        alert.addAction(UIAlertAction(title: "OK", style: .default) { _ in
            completionHandler()
        })
        self.present(alert, animated:true)
}

Similarly, if a web page’s JavaScript calls window.open, implement this method:

  • webView(_:createWebViewWith:for:windowFeatures:)

Your implementation can return nil, or else create a new WKWebView, get it into the interface, and return it.

Custom Schemes

Starting in iOS 11, you can feed data into a web page by implementing a custom URL scheme. When the web page asks for the data by way of the scheme, the WKWebView turns to your code to supply the data.

Let’s say I have an MP3 file called "theme" in my app’s asset catalog, and I want the user to be able to play it through an <audio> tag in my web page. I’ve invented a custom scheme that signals to my app that we want this audio data, and my web page’s <source> tag asks for its data using that scheme:

weak var wv: WKWebView!
let sch = "neuburg-custom-scheme-demo-audio" // custom scheme
override func viewDidLoad() {
    super.viewDidLoad()
    let config = WKWebViewConfiguration()
    // ... configure the web view ...
    let r = // ... CGRect for web view frame ...
    let wv = WKWebView(frame: r, configuration: config)
    self.view.addSubview(wv)
    self.wv = wv
    let s = """
    <!DOCTYPE html><html><head>
    <meta name="viewport" content="initial-scale=1.0, user-scalable=no" />
    </head><body>
    <p>Here you go:</p>
    <audio controls>
    <source src="(sch)://theme" />
    </audio>
    </body></html>
    """
    self.wv.loadHTMLString(s, baseURL: nil)
}

Now let’s fill in the missing code, where we configure the web view to accept sch as a custom scheme:

let sh = SchemeHandler()
sh.sch = self.sch
config.setURLSchemeHandler(sh, forURLScheme: self.sch)

The call to setURLSchemeHandler requires that we provide an object that adopts the WKURLSchemeHandler protocol. That object cannot be self, or we’ll get ourselves into a retain cycle (similar to the problem we had with WKScriptMessageHandler earlier); so I’m configuring and passing a custom SchemeHandler helper object instead.

The WKURLSchemeHandler protocol methods are where the action is. When the web page wants data with our custom scheme, it calls our SchemeHandler’s webView(_:start:) method. The second parameter is a WKURLSchemeTask that operates as our gateway back to the web view. Its request property contains the URLRequest from the web page. We must call the WKURLSchemeTask’s methods, first supplying a URLResponse, then handing it the data, then telling it that we’ve finished:

class SchemeHandler : NSObject, WKURLSchemeHandler {
    var sch : String?
    func webView(_ webView: WKWebView, start task: WKURLSchemeTask) {
        if let url = task.request.url,
            let sch = self.sch,
            url.scheme == sch,
            let host = url.host,
            let theme = NSDataAsset(name:host) {
                let data = theme.data
                let resp = URLResponse(url: url, mimeType: "audio/mpeg",
                    expectedContentLength: data.count,
                    textEncodingName: nil)
                task.didReceive(resp)
                task.didReceive(data)
                task.didFinish()
        } else {
            task.didFailWithError(NSError(domain: "oops", code: 0))
        }
    }
    func webView(_ webView: WKWebView, stop task: WKURLSchemeTask) {
        print("stop")
    }
}

The outcome is that the audio controls appear in our web page, and when the user taps the Play button, what plays is the MP3 file from the app’s asset catalog.

Warning

This feature works only if we create the web view ourselves, in code, using the init(frame:configuration:) initializer, with the WKWebViewConfiguration object prepared beforehand. So we can’t use a custom scheme with a web view instantiated from a nib. I regard this limitation as a bug.

Web View Previews and Context Menus

As I described earlier (“Previews and Context Menus”), 3D touch peek and pop was superseded in iOS 13 by the use of a long press to summon a preview with a context menu. If a WKWebView’s allowsLinkPreview property is true, the user can long press on a link to summon a preview of the linked page, along with default menu items Open Link, Add to Reading List, Copy Link, Share, and Hide Link Previews. (This property can be set in the nib editor.)

The default response to the user tapping on the preview is to open the link in Safari. This mechanism does not pass through your navigation delegate’s implementation of webView(_:decidePolicyFor:preferences:decisionHandler:). Instead, if you wish to customize your app’s response to the user previewing links, you can implement methods in your uiDelegate (WKUIDelegate) that are parallel to the UIContextMenuInteractionDelegate methods:

webView(_:contextMenuConfigurationForElement:completionHandler:

The element: is a WKContextMenuElementInfo, which is merely a value class carrying a single property, the linkURL for the link that the user is pressing on. Your job is to call the completionHandler with one of these as argument:

  • A fully configured UIContextMenuConfiguration created by calling the initializer init(identifier:previewProvider:actionProvider:). If you supply an action provider function, the incoming parameter is an array of the default UIActions, so you can keep any or all of those as menu items if you want to.

  • nil to permit the default behavior.

  • An empty UIContextMenuConfiguration (no preview provider, no action provider) to prevent anything from happening.

webView(_:contextMenuWillPresentForElement:)

The context menu is about to appear.

webView(_:contextMenuForElement:willCommitWithAnimator:)

The user has tapped the preview. You might add a completion handler to the animator to perform a view controller transition.

webView(_:contextMenuDidEndForElement:)

The preview and menu have been dismissed, no matter how.

Safari View Controller

A Safari view controller (SFSafariViewController) embeds 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, a Text button where the user can change the text size, enter Reader view, and configure site settings, plus a Refresh button, along with 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 (import SafariServices), create the SFSafariViewController, initialize it with a URL, and present it:

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

In this example, we interfere (as a WKWebView’s navigationDelegate) with the user tapping on a link in our web view, so that the linked page is displayed in an SFSafariViewController within our app:

func webView(_ webView: WKWebView,
    decidePolicyFor navigationAction: WKNavigationAction,
    preferences: WKWebpagePreferences, decisionHandler:
    @escaping (WKNavigationActionPolicy, WKWebpagePreferences) -> Void) {
        if navigationAction.navigationType == .linkActivated {
            if let url = navigationAction.request.url {
                let svc = SFSafariViewController(url: url)
                self.present(svc, animated: true)
                decisionHandler(.cancel, preferences)
                return
            }
        }
        decisionHandler(.allow, preferences)
}

When the user taps the Done button in the navigation bar, the Safari view controller is dismissed. You can change the title of the Done button; to do so, set the Safari view controller’s dismissButtonStyle to .done, .close, or .cancel.

You can set the color of the Safari view controller’s navigation bar (preferredBarTintColor) and bar button items (preferredControlTintColor). This allows the look of the view to harmonize with the rest of your app.

You can configure a Safari view controller by creating an SFSafariViewController.Configuration object and passing it to the Safari view controller through its initializer init(url:configuration:). Using the configuration object, you can prevent the Safari view controller’s top and bottom bars from collapsing when the user scrolls; to do so, set its barCollapsingEnabled property to false.

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.

func safariViewController(_:initialLoadDidRedirectTo:)

Reports that the Safari view controller’s initial web page differs from the URL you originally provided, because redirection occurred.

safariViewController(_:activityItemsFor:title:)

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

safariViewController(_:excludedActivityTypesFor:title:)

In a sense, the converse of the preceding: allows you to eliminate unwanted activity types from the Share button.

safariViewControllerWillOpenInBrowser(_:)

(New in iOS 14.) The user has tapped the button that reopens the current page in Mobile Safari.

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; mobile WebKit 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 (https://developer.apple.com/safari/).

A good place to start is the Safari Web Content Guide (in the documentation archive). 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:

<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: 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. The viewport’s user-scalable attribute can be treated as yes by setting the 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 (also known as a responsive web page); 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. New in iOS 14, the WKWebView mediaType property lets you set the string that will be matched by the CSS @media rule; in addition to the built-in screen and print types, you can supply your own type using this property.

Another new iOS 14 WKWebView property related to CSS is pageZoom; this is equivalent to the CSS zoom property, but it applies to all page content and is injected before the page even starts to load.

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 on a device or the Simulator, and lets you analyze every aspect of how it works. 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 (see Chapter 22). Additional helpful documentation includes Apple’s WebKit DOM Programming Topics and the WebKit JS framework reference page.

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

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