WKWebview Challenges

Way back since iOS 8, Apple have been recommending we switch web content in our app to use the newer, in many ways more powerful, WKWebview, and away from the OG web view, UIWebView. Its not unreasonable to assume UIWebView will be deprecated at some point in the future, so if you have a legacy app that is still using the older technology, its worth taking the time to migrate. WKWebView contains some great features over UIWebView - its runs as its own separate process, meaning you have all of the system’s optimisations for fetching and rendering web content. Theres also awesome new tools for evaluating javascript on the loaded page, meaning you can read elements in your code, be notified if a property changes, and run javascript commands to modify the loaded page. If this all sounds like something you’d like to take advantage of I recommend NSHipster‘s introduction to the changes in WKWebview.

But from my own recent experience in migrating to WKWebView, the new technology still doesn’t quite feel finished. Its obvious Apple have made some trade-off decisions to make WKWebView‘s performance as good as it can be, but this sometimes impacts what control we have over it as a developer. I wanted to share some of the challenges I faced, as I’ve not seen a ton of discussion of these. Hopefully you’ll be reading this and know where I’m going wrong, but if not, hopefully my pitfalls will help you avoid the same.

WKWebView can’t be used in storyboards

Anecdotally it appears adoption of WKWebview is not as high as you might expect when Apple is heavily hinting the alternative will be deprecated. My guess at why this might be is that the WKWebview can’t be used in storyboards in apps targeting anything lower than iOS 11. As we’re not too far away from iOS 12 I suspect this won’t be an issue too longer, as those of us supporting iOS x - 1 won’t have this restriction. You can still drag a Web View onto your storyboard, but Apple’s fix for this issue is to add a compile time warning that you’re using this prior to iOS 11.

Instead this has to be created programatically

1
2
3
4
let webConfiguration = WKWebViewConfiguration()
webView = WKWebView(frame: .zero, configuration: webConfiguration)
view.addSubview(webView)
// obvs add some constraints

WKWebView crashes if a user taps on an image

The heading is perhaps a little unfair, this is completely expected behaviour, it just might not be anticipated. As with any webpage on iOS, the user can long-press an image to bring up a share sheet, one of these share options is to save the image to the camera roll. This requires user permission. So if you don’t set NSPhotoLibraryAddUsageDescription in your info.plist you risk users accidentally crashing your app by hovering on an image just a little too long.

No access to the URLRequest body

This is where things get a little more specific to my use case. WKNavigationDelegate includes some helpful callbacks - in fact, thats one of the reasons I was looking to migrate. webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) allows your app to respond to errors in navigation - a 500 error returned by the server for example, that can’t be detected in UIWebView. However, as WKWebView is running entirely in its own process, theres little access to what is happening with requests made. The pre-navigation callback webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) returns a WKNavigationAction object, which includes the URLRequest. Except the request.httpBody is always nil. This means its not possible to determine navigation behaviour based on a form response. While there is a suggestion for getting body values on StackOverflow using the evaluateJavaScript command, this is not suitable for using on web pages you don’t control, and can’t guarantee there won’t be breaking changes, or on secure web pages where injecting javascript is not a great security decision.

Can’t intercept requests with NSRLProtocol

As the URLRequest is made out of process, its not possible to intercept this as you usually might intercept all other requests out of your app. This means its not possible to add custom headers, or in my case, mock the requests for testing. There are a couple of workarounds for this, neither are especially pretty.
Using webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) you can call cancel the navigation, add your custom header to the request, and make a new request. This creates another call to …decidepolicyfor…, so just make sure you have a check in there to prevent an infinite loop.

1
2
3
4
5
6
7
8
9
10
11
12
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {

var request = navigationAction.request

if request.value(forHTTPHeaderField: "myCustomHeaderFeid") != nil {
decisionHandler(.allow)
return
}

decisionHandler(.cancel)
request.addValue("myCustomHeaderValue", forHTTPHeaderField: "myCustomHeaderFeid") webView.load(request)
}

My requirement for this was to mock the response the web view is receiving, so for obvious reasons I don’t want to make changes to production code to facilitate mocking. The route I took was to use an Apple private API. As this is code that will never be included in the production app, there is no risk in this being flagged by Apple at app review. Including a category extension in a file only added to the testing target allows us to route URLRequests through the usual delegate in your app.

1
2
3
4
5
6
7
8
9
10
extension URLProtocol {
class func registerWebView(scheme: String!) {
if let contextController: AnyObject = NSClassFromString("WKBrowsingContextController") {
if contextController.responds(to: NSSelectorFromString("registerSchemeForCustomProtocol:")) {
let _ = contextController.perform(NSSelectorFromString("registerSchemeForCustomProtocol:"), with: scheme)
}
}
}
}
}

Then register http & https by calling

1
2
URLProtocol.registerWebView(scheme: "http") 
URLProtocol.registerWebView(scheme: "https")

Update 27/6/18

Apple WebKit engineer Brady Edison tweeted

So I asked him about the issue I’ve not been able to find a suitible workaround for, getting access to the POST request body.

I’d be delighted to find more concrete ways to solve these shortcomings, the extra power WKWebView has over UIWebView makes it a great alternative. But as it stands, for some use cases its still not really there, even with 3 yeras of refinement from Apple. So if you do work out a better solution, please let me know - rw@rwapp.co.uk