How To Use iOS WKWebView with UIWebView Fallback

One Web View to Rule Them All

Mobile Apps, Mobile Development, Tutorials Comments (10)

Feel free to read our earlier article on the latest Sandbox update if you’re just interested in what Sandbox can do, or read on if you get excited about categories and protocols!

Note: This blog post uses code written in Objective-C. A Swift code repository is available here and here is a companion blog post!

New in iOS 8: WKWebView

Image courtesy Apple
Image courtesy Apple

iOS 8 finally gave developers access to a WebKit-powered Web view, called simply WKWebView.

Previously, the much slower UIWebView was the only tool at an iOS developer’s disposal, but the new WKWebView allowed us to bring users the same 60fps high-performance browsing behind mobile Safari.

Sounds great!

There’s only one problem: this support is obviously only available in iOS 8 and up, meaning that any app that wants to take advantage of WebKit but supports iOS 7 is going to have to do some extra work behind the scenes to pick the right view for the user’s OS.

Luckily, we here at Float have already braved this wild frontier, and are here to provide some guidance.

Sandbox

sandbox_an_a_better_ipad

Sandbox is Float’s Web browser designed for kiosks, parents, and any other situation where the device’s owner might want to restrict what an end user is capable of doing within a Web browser on their device.

When Apple announced WKWebKit at WWDC 14, we knew we wanted to enable this functionality for Sandbox users.

However, we wanted to enable WKWebKit in iOS 8 without leaving iOS 7 users in the lurch. We also didn’t want to duplicate a lot of code: we needed a minimal, simple way to abstract the differences between the two, and in the rest of this post, that’s what we’ll cover.

Click here for a video on how to use Sandbox for iOS.

Step One: Detection

Before we start, all code in this post is available in this GitHub repo, which is a full working example of our methodology. Check it out!

Perhaps the most obvious first step is how to determine whether or not to use WKWebKit at runtime.

Although WKWebKit has a WK_API_ENABLED define, that won’t do us much good as it will always be determined at compile time, not run time.

We could check the user’s version of iOS and verify that it’s 8.0 or greater, but what if Apple removes WKWebKit in iOS 9?

Ultimately we decided on this implementation:

if (NSClassFromString(@"WKWebView")) {
    _webView = [[WKWebView alloc] initWithFrame: [[self view] bounds]];
} else {
    _webView = [[UIWebView alloc] initWithFrame: [[self view] bounds]];
}

This checks for the availability of WKWebView without depending on any other element of the OS and then instantiates a Web view.

Of course, you’ve probably noticed that this code won’t work right out of the box. We’ve got to set up a protocol first.

Step Two: Protocol

Protocols in Objective-C are similar to an interface in a language like Java; they simply define a set of capabilities.

In our case, we want to create one protocol that will define what our custom WKWebView and UIWebView (which we’ll create later on) can do. Let’s start with this:

@protocol FLWebViewProvider <NSObject>

- (void) setDelegateViews: (id) delegateView;

@end

This protocol, which we have in its own file (FLWebViewProvider.h), will be the basis for our future Web views.

Right now, it only has one method: setDelegateViews, which allows us to assign a class as the Web view’s delegate. This makes things a bit easier for us later on, as we don’t have to worry about setting delegates depending on which Web view is available in our view controller. We’ll add more to this later, but let’s start on our UIWebView category.

Step Three: Categories

A category allows you to add behavior to an existing class.

We chose this rather than subclassing since the documentation for UIWebView states that “The UIWebView class should not be subclassed.” While it’s possible we could subclass without causing technical issues, we wanted to avoid a possible app rejection.

In our case, we’ll need two categories: one to bring UIWebView in line with our FLWebViewProvider protocol, and one to bring WKWebView in line with our FLWebViewProvider protocol. Once both adhere to the same protocol, we can use them interchangeably in our application.

First, a header file for our UIWebView:

#import <UIKit/UIKit.h>

@interface UIWebView (FLUIWebView) <FLWebViewProvider>

- (void) setDelegateViews: (id <UIWebViewDelegate>) delegateView;

@end

All we really need to do here is specify that our setDelegateViews method takes a UIView that conforms to UIWebViewDelegate. Then, in the implementation:

#import "UIWebView+FLUIWebView.h"

@implementation UIWebView (FLWebView)

- (void) setDelegateViews: (id <UIWebViewDelegate>) delegateView
{
    [self setDelegate: delegateView];
}

@end

UIWebView already has a delegate property, so our method just needs to set this value via its setter. Easy! There are a lot more issues to iron out, but this is a good start.

Now we need to get on to our WKWebView category. Here’s the header file:

#import <WebKit/WebKit.h>
#import "FLWebViewProvider.h"

@interface WKWebView (FLWKWebView) <FLWebViewProvider>

- (void) setDelegateViews: (id <WKNavigationDelegate, WKUIDelegate>) delegateView;

@end

Again, we’re just specifying that the delegate passed to setDelegateViews should conform to WKNavigationDelegate and WKUIDelegate, which are similar to UIWebView’s UIWebViewDelegate. The implementation is very similar to before:

#import "WKWebView+FLWKWebView.h"

@implementation WKWebView (FLWKWebView)

- (void) setDelegateViews: (id <WKNavigationDelegate, WKUIDelegate>) delegateView
{
    [self setNavigationDelegate: delegateView];
    [self setUIDelegate: delegateView];
}

@end

Now our view controller can call setDelegateViews on a Web view that implements our protocol, whether it’s a WKWebView or UIWebView.

Let’s set it up!

Step Four: View Controller

Our view controller’s interface doesn’t need much:

#import <UIKit/UIKit.h>
#import <WebKit/WebKit.h>
#import "FLWebViewProvider.h"

@interface ViewController : UIViewController <UIWebViewDelegate, WKNavigationDelegate, WKUIDelegate>

@property (nonatomic) UIView <FLWebViewProvider> *webView;

@end

Our webView property will conform to FLWebViewProvider, which is what we wanted.

Also, for this example project, we’ve made ViewController the delegate for our Web view. You could have another class to serve this purpose, but it works well for this small project.

Here’s where things start to get interesting:

#import "ViewController.h"
#import "UIWebView+FLUIWebView.h"
#import "WKWebView+FLWKWebView.h"

@interface ViewController ()

@end

@implementation ViewController

- (void) viewDidLoad {
    [super viewDidLoad];

    if (NSClassFromString(@"WKWebView")) {
        _webView = [[WKWebView alloc] initWithFrame: [[self view] bounds]];
    } else {
        _webView = [[UIWebView alloc] initWithFrame: [[self view] bounds]];
    }

    [[self view] addSubview: [self webView]];
    [[self webView] setDelegateViews: self];
}

@end

As you probably know, viewDidLoad gets called after the view loads.

After that, you probably recognize the lines where we instantiate our Web view.

Note that we have to import our categories for Xcode to see them. This will let Xcode know that UIWebView and WKWebView have been more or less modified with new methods.

After that, we can add the Web view as a subview and use our setDelegateViews method to set the view controller as the delegate for our Web view. Seems like a lot of work for one little method! But, now we can talk about making our One WebView better, and then get on to a discussion of delegate methods.

Step Five: Expanding

Being able to set the delegate views like that is handy, but what we really want is some standardization between the two classes. This NSHipster post summarizes the differences well.

Let’s simplify things.

First, we can add these to our protocol:

@property (nonatomic, strong) NSURLRequest *request;

@property (nonatomic, strong) NSURL *URL;

- (void) loadRequest: (NSURLRequest *) request;

- (void) evaluateJavaScript: (NSString *) javaScriptString completionHandler: (void (^)(id, NSError *)) completionHandler;

These are the properties and methods we want our categories to adhere to.

WKWebView doesn’t have the request property, so by defining it here, we’re saying that our WKWebView category should have it (this doesn’t impact UIWebView, since it already exists there).

Conversely, UIWebView doesn’t have the URL property, so we’ll need to implement that. loadRequest exists in both already, so we don’t actually need to do anything there. evaluateJavaScript is a WKWebView method, and UIWebView has something comparable, but we’ll need to change the implementation a bit there to make it work the same for both views.

Since our categories have already agreed to conform to this protocol, we don’t need to add anything to their headers. Instead, we just need to add the implementations. Here are the changes to UIWebView’s implementation:

- (NSURL *) URL
{
    return [[self request] URL];
}

- (void) evaluateJavaScript: (NSString *) javaScriptString completionHandler: (void (^)(id, NSError *)) completionHandler
{
    NSString *string = [self stringByEvaluatingJavaScriptFromString: javaScriptString];

    if (completionHandler) {
        completionHandler(string, nil);
    }
}

Since we can get the URL property from within UIWebView’s request property, it’s pretty simple to create a shorthand method to grab it.

UIWebView has stringByEvaluatingJavaScriptFromString but WKWebView has evaluateJavaScript, which is asynchronous.

Rather than trying to force WKWebView’s method to be synchronous, we merely call UIWebView’s method internally and pass the result to the callback.

Since UIWebView already had loadRequest and the request property, it now conforms to the FLWebViewProvider!

Now we need to turn our attention to the WKWebView category:

#import <objc/runtime.h>

...

- (NSURLRequest *) request
{
    return objc_getAssociatedObject(self, @selector(request));
}

- (void) setRequest: (NSURLRequest *) request
{
    objc_setAssociatedObject(self, @selector(request), request, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

Yes, we’re using the dreaded associated objects! NSHipster has a great discussion of these here, so I won’t go into too much detail, but essentially this is the only way to add custom properties in a category. This allows us to treat the request property the same between UIWebView and WKWebView, even though the former has it and the latter does not.

However, the request property won’t be set for us when we call loadRequest. In order for that to happen, we’ve got to swizzle.

Step Six: Swizzling

Once again, Objective-C proves to be a powerful language with some oft-unused functionality that enables us to bend the rules a bit.

Once again, NSHipster has covered this, but I’ll briefly say that method swizzling allows us to swap out a built-in function with our own.

In our case, we want to swap out the built-in WKWebView loadRequest with our own loadRequest method that will simply update the request property. We want this to be done once, on initial load, so we’ll use WKWebView’s load class method.

+ (void) load
{
    static dispatch_once_t onceToken;

    dispatch_once(&onceToken, ^{
        Class class = [self class];

        SEL originalSelector = @selector(loadRequest:);
        SEL swizzledSelector = @selector(altLoadRequest:);

        Method originalMethod = class_getInstanceMethod(class, originalSelector);
        Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);

        BOOL didAddMethod = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));

        if (didAddMethod) {
            class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
        } else {
            method_exchangeImplementations(originalMethod, swizzledMethod);
        }
    });
}

We create a token to ensure that this is only done once, then do the bulk of the work inside a block.

After getting a reference to the current class, we create references to the selectors for the original method (loadRequest) and our new method (altLoadRequest), and then get references to the methods themselves.

We then attempt to add our new method in place of the original. Since class_addMethod returns a boolean value indicating success, then we can store this result and, if successful, put the original method in place of our alternative. If we weren’t successful, we use an alternative process to swap the two methods. This is some dangerous stuff we’re messing with, so take a look at the runtime reference if you want more information.

The end result of all of this is that loadRequest will swap with altLoadRequest, which is a pretty simple little method:

- (void) altLoadRequest: (NSURLRequest *) request
{
    [self setRequest: request];
    [self altLoadRequest: request];
}

It just sets the request property, and then calls altLoadRequest, which at runtime is actually the original loadRequest.

The end result of all of this is that WKWebView will now have a request property identical to the UIWebView request property.

Step Seven: Delegates

Back in our view controller, we probably want to handle UIWebView and WKWebView delegate methods the same way. For our purposes, we created the following methods:

- (BOOL) shouldStartDecidePolicy: (NSURLRequest *) request
{
    return YES;
}

- (void) didStartNavigation
{
    // do stuff
}

- (void) failLoadOrNavigation: (NSURLRequest *) request withError: (NSError *) error
{
    // do stuff
}

- (void) finishLoadOrNavigation: (NSURLRequest *) request
{
    // do stuff
}

These will be our shared delegate methods, which are called by both UIWebView’s and WKWebView’s delegate methods. UIWebViewDelegate is, of course, its own protocol with a few methods of interest, which we’ll implement like so:

- (BOOL) webView: (UIWebView *) webView shouldStartLoadWithRequest: (NSURLRequest *) request navigationType: (UIWebViewNavigationType) navigationType
{
    return [self shouldStartDecidePolicy: request];
}

- (void) webViewDidStartLoad: (UIWebView *) webView
{
    [self didStartNavigation];
}

- (void) webView: (UIWebView *) webView didFailLoadWithError: (NSError *) error
{
    [self failLoadOrNavigation: [webView request] withError: error];
}

- (void) webViewDidFinishLoad: (UIWebView *) webView
{
    [self finishLoadOrNavigation: [webView request]];
}

As you can see, these will all call our shared delegate methods.

The latter two methods were set up to receive a request, so we have to get the request from the webView property, but you could just as easily set up the shared delegate methods to accept a webView property directly, or even accept no Web view and instead reference the view controller webView property itself, assuming you only have the one to worry about.

WKWebView’s delegate methods are a bit different, but they’re similar in that they ultimately just defer to our shared methods:

- (void) webView: (WKWebView *) webView decidePolicyForNavigationAction: (WKNavigationAction *) navigationAction decisionHandler: (void (^)(WKNavigationActionPolicy)) decisionHandler
{
    decisionHandler([self shouldStartDecidePolicy: [navigationAction request]]);
}

- (void) webView: (WKWebView *) webView didStartProvisionalNavigation: (WKNavigation *) navigation
{
    [self didStartNavigation];
}

- (void) webView:(WKWebView *) webView didFailProvisionalNavigation: (WKNavigation *) navigation withError: (NSError *) error
{
    [self failLoadOrNavigation: [webView request] withError: error];
}

- (void) webView: (WKWebView *) webView didFailNavigation: (WKNavigation *) navigation withError: (NSError *) error
{
    [self failLoadOrNavigation: [webView request] withError: error];
}

- (void) webView: (WKWebView *) webView didFinishNavigation: (WKNavigation *) navigation
{
    [self finishLoadOrNavigation: [webView request]];
}

The first method passes in a reference to a callback, but it’s just looking for a boolean value, so we pass in the result of our shared shouldStartDecidePolicy method. In some cases, we want to pass an NSURLRequest into our shared methods, but since we now have a request property in WKWebView, it’s easy to get it.

Final Thoughts

This is meant to be a brief introduction to how we ultimately decided to implement WKWebView for our application.

Feel free to peruse our GitHub repo (MIT licensed) to see thoroughly commented code with more detail about implementation and some additional methods. You can even download the repo and build it yourself; see the readme for details.

If you have any more questions, feel free to contact us or drop a line in the comments below. If you’ve spotted a problem with our code (it happens!) or want to see more, feel free to open an issue or pull request on GitHub.

That’s it for now! Enjoy the blazing speed of WKWebView!

Follow Float
The following two tabs change content below.

» Mobile Apps, Mobile Development, Tutorials » How To Use iOS WKWebView...
On December 3, 2014
By
, , , , ,

10 Responses to How To Use iOS WKWebView with UIWebView Fallback

  1. Ivo says:

    We ran into something really strange. The NSClassFromString(@”WKWebView”) approach works fine, except in one peculiar case. On an iPhone 4 with iOS 7.1 and an app compiled in Release (not debug), this call will return a non-nil value and thus tries to instantiate a WKWebView (which will then fail).

    We discovered this after an app submit, users on this particular device reporting a failing app while in our environments it worked. Not sure what is causing this, but we discovered that using objc_getClass was more reliable than NSClassFromString.

  2. Antonio says:

    Really good explanation, but I think there is a big chance for your to be rejected if you use Method Swizzling

    • Steve Richey says:

      If you’re referring to the oft-cited case of Three20’s notice from Apple regarding their use of method swizzling, the issue in their case was not that swizzling was being used, but rather that elements of the API that were changing in a future updated were being swizzled. As a result of this API change, Apple felt that the application would be likely to crash, and were warning that they may have to pull the application from the store.

      Method swizzling is a standard part of the Objective-C runtime, and not only is it unlikely that Apple would know that swizzling is being implemented, but in and of itself, that would not be cause for rejection. However, an issue could arise if you were to use swizzling to override private APIs.

      Furthermore, we’re using the aforementioned implementation of swizzling in our application, Sandbox. Thus far, this application has not been rejected, and we have not been warned by Apple in regards to our implementation.

      I hope this clarifies things, and thank you for your comment!

  3. Andrew Halls says:

    There was some discussion back in September ’14 that apps using a similar technique to use WKWebView on iOS 8 and fallback to UIWebView on iOS 7 ran into app store validation issues. Was that just a temporary concern, or do you need to dynamically load WebKit framework as suggested in this post:

    http://stackoverflow.com/questions/25897123/including-webkit-framework-for-ios8-fails-validation

    • Steve Richey says:

      We haven’t had any issues getting Sandbox validated, using the same implementation as is mentioned in this post, with device support going back to iOS 6. So, while it might have been an issue at one point, this method certainly was accepted in our case!

  4. Johanna says:

    If you would like to implement stringByEvaluatingJavaScriptFromString for WKWebView, you can simply use semaphores from GCD:

    – (NSString *)stringByEvaluatingJavaScriptFromString:(NSString *)script {
    __block id evalResult;
    dispatch_semaphore_t waitSemaphore = dispatch_semaphore_create(0);

    [self evaluateJavaScript:script completionHandler:^(id result, NSError *error) {
    evalResult = result;
    dispatch_semaphore_signal(waitSemaphore);
    }];

    dispatch_semaphore_wait(waitSemaphore, DISPATCH_TIME_FOREVER);

    return evalResult ? [NSString stringWithFormat:@”%@”, evalResult] : @””;
    }

    • Steve Richey says:

      Good idea! Asynchronous JavaScript worked well for us, but if you wanted the implementation to be synchronous, this would be a great way to do it.

    • Johanna says:

      Or if you want to bail out after so much time spent waiting for the response:

      NSDate *bailTime = [NSDate dateWithTimeIntervalSinceNow:2.5];
      while (dispatch_semaphore_wait(waitSemaphore, DISPATCH_TIME_NOW)) {
      if ([bailTime compare:[NSDate date]] == NSOrderedAscending)
      return @””;

      [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]];
      }

      I see what you’re saying in the comments of the code about it being a hard problem though. There are just so many instances where in order to truly replace WKWebKit for UIWebKit (e.g. youtube-ios-player-helper) you need a blocking string evaluation.

  5. Antonio says:

    How did you manage to share cookies between WKWebView and NSURLSessions? By default WKWebView doesn’t use NSHTTPCookieStorage. I can set cookies manually to the request but it doesn’t seem to be the best approach.

Leave a Reply

Your email address will not be published. Required fields are marked *

« »