WKWeb​View

Written by Mattt Thompson

iOS has a complicated relationship with the web. And it goes back to the very inception of the platform nearly a decade ago.

It’s difficult to appreciate just how differently the first iPhone could have turned out. The iconic touchscreen device we know and love today was just one option on the table. Early prototypes explored the use of a physical keyboard and a touch screen + stylus combo, with screen dimensions going up to 5" x 7". Even the iPod click wheel was a serious contender for a time.

But perhaps the most significant early decision to be made involved software, not hardware.

How should the iPhone run software? Apps, like on OSX, or as web pages, using Safari? That choice to fork OS X and build iPhoneOS had widespread implications, and remains a contentious decision to this day.

Consider this infamous line from Steve Jobs' WWDC 2007 keynote:

The full Safari engine is inside of iPhone. And so, you can write amazing Web 2.0 and Ajax apps that look exactly and behave exactly like apps on the iPhone. And these apps can integrate perfectly with iPhone services. They can make a call, they can send an email, they can look up a location on Google Maps.

The web has always been a second-class citizen on iOS (which is ironic, since the iPhone is largely responsible for the mobile web existing as it does today). UIWebView is massive and clunky and leaks memory. It lags behind Mobile Safari, which has the benefit of the Nitro JavaScript engine.

However, all of this has changed with the introduction of WKWebView and the rest of the WebKit framework.


WKWebView is the centerpiece of the modern WebKit API introduced in iOS 8 & OS X Yosemite. It replaces UIWebView in UIKit and WebView in AppKit, offering a consistent API across the two platforms.

Boasting responsive 60fps scrolling, built-in gestures, streamlined communication between app and webpage, and the same JavaScript engine as Safari, WKWebView is one of the most significant announcements to come out of WWDC 2014.

What was a single class and protocol with UIWebView & UIWebViewDelegate has been factored out into 14 classes and 3 protocols in WKWebKit. Don’t be alarmed by the huge jump in complexity, though—this new architecture allows for a ton of new features:

WKWebKit Framework

Classes

  • WKBackForwardList: A list of webpages previously visited in a web view that can be reached by going back or forward.
    • WKBackForwardListItem: Represents a webpage in the back-forward list of a web view.
  • WKFrameInfo: Contains information about a frame on a webpage.
  • WKNavigation: Contains information for tracking the loading progress of a webpage.
    • WKNavigationAction: Contains information about an action that may cause a navigation, used for making policy decisions.
    • WKNavigationResponse: Contains information about a navigation response, used for making policy decisions.
  • WKPreferences: Encapsulates the preference settings for a web view.
  • WKProcessPool: Represents a pool of Web Content processes.
  • WKUserContentController: Provides a way for JavaScript to post messages and inject user scripts to a web view.
    • WKScriptMessage: Contains information about a message sent from a webpage.
    • WKUserScript: Represents a script that can be injected into a webpage.
  • WKWebViewConfiguration: A collection of properties with which to initialize a web view.
  • WKWindowFeatures: Specifies optional attributes for the containing window when a new web view is requested.

Protocols

  • WKNavigationDelegate: Provides methods for tracking the progress of main frame navigations and for deciding load policy for main frame and subframe navigations.
  • WKScriptMessageHandler: Provides a method for receiving messages from JavaScript running in a webpage.
  • WKUIDelegate: Provides methods for presenting native user interface elements on behalf of a webpage.

API Diff Between UIWebView & WKWebView

WKWebView inherits much of the same programming interface as UIWebView, making it convenient for apps to migrate to WKWebKit (and conditionally compile as necessary while iOS 8 gains widespread adoption).

For anyone interested in the specifics, here’s a comparison of the APIs of each class:

UIWebView WKWebView
var scrollView: UIScrollView { get } var scrollView: UIScrollView { get }
var configuration: WKWebViewConfiguration { get }
var delegate: UIWebViewDelegate? var UIDelegate: WKUIDelegate?
var navigationDelegate: WKNavigationDelegate?
var backForwardList: WKBackForwardList { get }

Loading

UIWebView WKWebView
func loadRequest(request: NSURLRequest) func loadRequest(request: NSURLRequest) -> WKNavigation?
func loadHTMLString(string: String, baseURL: NSURL?) func loadHTMLString(string: String, baseURL: NSURL) -> WKNavigation?
func loadData(data: NSData, MIMEType: String, textEncodingName: String, baseURL: NSURL)
var estimatedProgress: Double { get }
var hasOnlySecureContent: Bool { get }
func reload() func reload() -> WKNavigation?
func reloadFromOrigin() -> WKNavigation?
func stopLoading() func stopLoading()
var request: NSURLRequest? { get }
var URL: NSURL? { get }
var title: String? { get }

History

UIWebView WKWebView
func goToBackForwardListItem(item: WKBackForwardListItem) -> WKNavigation?
func goBack() func goBack() -> WKNavigation?
func goForward() func goForward() -> WKNavigation?
var canGoBack: Bool { get } var canGoBack: Bool { get }
var canGoForward: Bool { get } var canGoForward: Bool { get }
var loading: Bool { get } var loading: Bool { get }

Javascript Evaluation

UIWebView WKWebView
func stringByEvaluatingJavaScriptFromString(script: String) -> String
func evaluateJavaScript(javaScriptString: String, completionHandler: ((AnyObject?, NSError?) -> Void)?)

Miscellaneous

UIWebView WKWebView
var keyboardDisplayRequiresUserAction: Bool
var scalesPageToFit: Bool
var allowsBackForwardNavigationGestures: Bool

Pagination

WKWebView currently lacks equivalent APIs for paginating content.

  • var paginationMode: UIWebPaginationMode
  • var paginationBreakingMode: UIWebPaginationBreakingMode
  • var pageLength: CGFloat
  • var gapBetweenPages: CGFloat
  • var pageCount: Int { get }

Refactored into WKWebViewConfiguration

The following properties on UIWebView have been factored into a separate configuration object, which is passed into the initializer for WKWebView:

  • var allowsInlineMediaPlayback: Bool
  • var mediaPlaybackRequiresUserAction: Bool
  • var mediaPlaybackAllowsAirPlay: Bool
  • var suppressesIncrementalRendering: Bool

JavaScript ↔︎ Swift Communication

One of the major improvements over UIWebView is how interaction and data can be passed back and forth between an app and its web content.

Injecting Behavior with User Scripts

WKUserScript allows JavaScript behavior to be injected at the start or end of document load. This powerful feature allows for web content to be manipulated in a safe and consistent way across page requests.

As a simple example, here’s how a user script can be injected to change the background color of a web page:

let source = "document.body.style.background = \"#777\";"
let userScript = WKUserScript(source: source, injectionTime: .AtDocumentEnd, forMainFrameOnly: true)

let userContentController = WKUserContentController()
userContentController.addUserScript(userScript)

let configuration = WKWebViewConfiguration()
configuration.userContentController = userContentController
self.webView = WKWebView(frame: self.view.bounds, configuration: configuration)

WKUserScript objects are initialized with JavaScript source, as well as whether it should be injected at the start or end of loading the page, and whether this behavior should be used for all frames, or just the main frame. The user script is then added to a WKUserContentController, which is set on the WKWebViewConfiguration object passed into the initializer for WKWebView.

This example could easily be extended to perform more significant modifications to the page, such as removing advertisements, hiding comments, or maybe changing all occurrences of the phrase “the cloud” to “my butt”.

Message Handlers

Communication from web to app has improved significantly as well, with message handlers.

Just like console.log prints out information to the Safari Web Inspector debug console, information from a web page can be passed back to the app by invoking:

window.webkit.messageHandlers.{NAME}.postMessage()

What’s really great about this API is that JavaScript objects are automatically serialized into native Objective-C or Swift objects.

The name of the handler is configured in addScriptMessageHandler(), which registers a handler conforming to the WKScriptMessageHandler protocol:

class NotificationScriptMessageHandler: NSObject, WKScriptMessageHandler {
    func userContentController(userContentController: WKUserContentController, didReceiveScriptMessage message: WKScriptMessage!) {
        print(message.body)
    }
}

let userContentController = WKUserContentController()
let handler = NotificationScriptMessageHandler()
userContentController.addScriptMessageHandler(handler, name: "notification")

Now, when a notification comes into the app, such as to notify the creation of a new object on the page, that information can be passed with:

window.webkit.messageHandlers.notification.postMessage({body: "..."});

Add User Scripts to create hooks for webpage events that use Message Handlers to communicate status back to the app.

The same approach can be used to scrape information from the page for display or analysis within the app.

For example, if someone were to build a browser specifically for NSHipster.com, it could have a button that listed related articles in a popover:

// document.location.href == "http://nshipster.com/webkit"
function getRelatedArticles() {
    var related = [];
    var elements = document.getElementById("related").getElementsByTagName("a");
    for (i = 0; i < elements.length; i++) {
        var a = elements[i];
        related.push({href: a.href, title: a.title});
    }

    window.webkit.messageHandlers.related.postMessage({articles: related});
}
let js = "getRelatedArticles();"
self.webView?.evaluateJavaScript(js) { (_, error) in
    print(error)
}

// Get results in previously-registered message handler

If your app is little more than a thin container around web content, WKWebView is a game-changer. All of that performance and compatibility that you’ve longed for is finally available. It’s everything you might have hoped for.

If you’re more of a native controls purist, you may be surprised at the power and flexibility afforded by the new technologies in iOS 8. It’s a dirty secret that some stock apps like Messages use WebKit to render tricky content. The fact that you probably haven’t noticed should be an indicator that web views actually have a place in app development best practices.