EXPEDIA GROUP TECHNOLOGY — SOFTWARE

Migrating to WKWebView

With deprecation looming, it is time to move away from UIWebView, and fully embrace WebKit for embedding web experiences.

Simon Haycock
Expedia Group Technology

--

Photo by Caspar Camille Rubin on Unsplash

Deprecation alarm bells

In the Hotels.com™ iOS app, a web view is presented for some of the user journey. For example, when the user is signing in, or checking their loyalty status. UIWebView from UIKit was used to handle this. We had briefly looked at using the newer web view class WKWebView from WebKit, but couldn’t see a big enough gain from switching. However, alarm bells rang when, in 2017, Apple announced that UIWebView would be deprecated. Initially, this was scheduled for iOS 11, but Apple delayed the deprecation to iOS 12. It was time to turn our backs on our old friend UIWebView, and to fully embrace WKWebView for all our embedded web views.

Benefits of migrating

UIWebView is over ten years old, and has watched longingly as front-end web technologies have evolved rapidly out of reach of its capabilities. WKWebView was first made available in iOS 8 and has had time for early issues to be ironed out. In addition to having the all-clear to drop iOS 12 when the time comes, there is a lot to gain from adopting this new web view class:

  • WKWebView is equipped with the Nitro JavaScript engine, allowing in-app web experiences to finally be on par with Safari.
  • This web view runs out-of-process, which means if a page causes WKWebView to crash, it does not crash your app.
  • ‘Messages’ can be received from Javascript by implementing a message handler. For example, if the page runs this…
window.webkit.messageHandlers.notification.postMessage({body: "..."});

…a WKScriptMessageHandler object can be informed of the message, provided it has been added to WKWebView’s configuration, via an instance of WKUserContentController.

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")
let configuration = WKWebViewConfiguration()
configuration.userContentController = userContentController
  • The property estimatedProgress can be used to inform the user how much of the page is still to be loaded.
  • WKWebView also supports better handling of alerts. If a page runs this Javascript…
alert("Hello!");

…the UIDelegate of the WKWebView is informed of the alert in the runJavaScriptAlertPanelWithMessage: method.

public func webView(webView: WKWebView, runJavaScriptAlertPanelWithMessage message: String, initiatedByFrame frame: WKFrameInfo, completionHandler: (() -> Void)) {
print(message)// or show a UIAlertController
completionHandler()
}

Implementation challenges

Delegates

WKWebView takes two delegates, a WKNavigationDelegate for handling decisions around navigation, and a WKUIDelegate for window management and responding to Javascript executed on the page.

You can map your four existing UIWebViewDelegate method implementations to these equivalent WKNavigationDelegate methods:

UIWebViewDelegate: webView(_:shouldStartLoadWith:navigationType:)
===>
WKNavigationDelegate: webView(_:decidePolicyFor action:decisionHandler:)
UIWebViewDelegate: webViewDidStartLoad(_:)
===>
WKNavigationDelegate: webView(_:didStartProvisionalNavigation:)
UIWebViewDelegate: webViewDidFinishLoad(_:)
===>
WKNavigationDelegate: webView(_:didFinish:)
UIWebViewDelegate: webView(_:didFailLoadWithError:)
===>
WKNavigationDelegate: webView(_:didFail:withError:)

In addition, watch out for:

  • webView(_:didFailProvisionalNavigation:withError:): this method can also be called when the loading of a page fails, so handle failure here too. It was found that this method can be called when a server is taking too long to respond (domain WebKitErrorDomain, code 102), so handle this accordingly.
  • webView(_:decidePolicyFor response:decisionHandler:): in addition to deciding whether to allow a navigation action (user tapping a link), we can also decide whether to accept the response of the navigation.

Link Preview

When you tap-and-hold on a link, the default behaviour is for the linked page to open in a small preview. If a web view is used to seamlessly meld with a native experience, it is wise to set the allowsLinkPreview property on WKWebView to false.

target=_blank

By default, if a link is declared in HTML as opening in a new window (ie, with target=_blank in the <a…> tag), it will not be displayed in WKWebView. This may be the desired behaviour. However to get around this, you can implement the WKUIDelegate window-handling method, and pass the request on to the WKWebView instance.

public func webView(_ webView: WKWebView, createWebViewWith configuration: WKWebViewConfiguration, for navigationAction: WKNavigationAction, windowFeatures: WKWindowFeatures) -> WKWebView? {
if navigationAction.targetFrame == nil {
webView.load(navigationAction.request)
}
return nil
}

Cookies

Differences in cookie handling

If you had cookies stored in HTTPCookieStorage, our old friend UIWebView would kindly add relevant cookies from this store automatically into the request headers. Likewise, any cookies set by the response would land in the same store.

When WKWebView was released into the wild in iOS 8, there was no way of handling cookies at all, other than getting / setting cookies by passing javascript to the page via the evaluateJavaScript method on WKWebView.

WKHTTPCookieStore

As of iOS 11, Apple introduced a new cookie store, WKHTTPCookieStore. You do not instantiate one of these directly, or access a singleton like with HTTPCookieStorage. Instead, you access WKWebView’s dedicated cookie store via its WKWebsiteDataStore, which is part of the WKWebViewConfiguration object used to instantiate WKWebView.

Unlike UIWebView, a WKWebView instance has its own data store WKWebsiteDataStore, which houses cookies, caches, databases, and any other form of local storage required by the page. We can query the site’s data using WKWebsiteDataStore’s API.

WKWebsiteDataStore types

It is possible to have two types of datastore; default and nonPersistent. Although the documentation at the time of writing is somewhat brief, we can assume that default will store data to storage and be accessible when loading a web view again with the same request. In contrast, nonPersistent will not persist any data from the page after the web view has been deallocated.

We had some success with using the default store. To our surprise, without any configuration, cookies from HTTPCookieStorage were included in a request made by WKWebView! I spoke to the WebKit guys at WWDC 2019, and there is indeed a secret link between the system-wide persisted cookie store and WKHTTPCookieStorage, however this link is complex (remember that WKHTTPCookieStorage, like WKWebView, runs out-of-process, hence the async get / set discussed later on). We found using a default datastore to be unreliable, with unpredictable behaviour. Another solution was needed.

Getting and setting cookies

We had to somehow guarantee that relevant cookies would be made available to WKWebView, and for cookies set in the response to be stored and made available for future network requests. Adding cookies into the request header manually works great for the first request, but subsequent requests will be missing those cookies, and no way was found to intercept the next request being made by WKWebView. It’s also not possible to pick cookies out of the response headers, as the Set-cookie header is never passed to the WKNavigationDelegate. We also looked into adding a WKHTTPCookieStoreObserver on WKHTTPCookieStorage, but found this to be unreliable also, with sporadic chunks of repeating observations made even when the web view was not loading anything. This also adds another asynchronous problem: If we are waiting to hit a ‘success’ URL before dismissing the web view automatically (as in the case of signing in the user), can we be sure that the cookie observer will be called after the page load has completed, and that cookies in the response have been saved? This is not guaranteed…

Our solution

After a lot of testing, debugging, head-scratching, diagram drawing, and cookie-scrutinising, we adopted this approach:

  • We create a nonPersistent datastore.
  • Cookies required in requests are copied into this datastore.
  • WKWebView is instantiated with a configuration which uses this datastore, and the initial request is loaded.
  • When the web experience is complete, cookies from the nonPersistent store are copied to the shared HTTPCookieStorage, where they can be used by future network requests. The web view is deallocated, and its datastore disappears.

This approach allows us to implement something that is predictable and makes us less reliant on poorly documented shared cookie handling by WebKit classes.

Note: We tried to add cookies to the datastore after the WKWebView is instantiated (via its configuration property), but this approach was unreliable at the time of writing. This may have been fixed, so experiment!

Asynchronous cookies on a single thread

To get and set cookies on WKHTTPCookieStore, you have to call an asynchronous method with a completion handler. You have to call it on the main thread, and the completion handler is called on the main thread, so waiting using a DispatchQueue or some such forced synchronicity is out of the question. This now means that setting up your web view and making the initial request is delayed to some time later on the main thread. If setting up the webview in a view controller during viewDidLoad, make sure there are no visual glitches whilst the view is being created.

Wrap Up

Moving to WKWebView required us to shoe-horn this class into the hole left by UIWebView, and for the most part, this was fairly straightforward.

The real issue for us was handling cookies, and whilst our solution is not great, at least it is predictable. As mentioned in a few places above, Apple has potentially improved the way they handle cookies in WebKit, and it would be great to see how others are solving the same issues as we faced.

Photo by Caspar Camille Rubin on Unsplash

https://lifeatexpedia.com/brands

--

--