Calling Objective-C methods from JavaScript in a UIWebView

Sep 28, 2012 09:00 · 589 words · 3 minute read iOS javascript objectivec uiwebview

The East Coast Time Line app I’ve just finished building includes a 360-degree virtual reality panorama section, which puts you “inside” several of the coaches in the National Railway Museum’s collection.  It’s created with the KRPano plugin that uses a set of tiles to build an HTML5 plugin that can be zoomed and rotated, either manually or with the aid of the device’s gyroscope.

The panorama itself is full-screen, so to provide a navigation back to the rest of the app there’s a floating toolbar that drops down from the top of the view in response to a tap.  Implementing that isn’t tricky - a UITapGestureRecognizer is attached to the UIWebView, and that triggers a method that changes the frame of the toolbar view from a negative to a positive Y value.

A problem arose when hotspots were added to the panorama - there are various items in the image that can be tapped to pop up an information “blob”.  That’s all done in jQuery inside the webView - so every time a hotspot was tapped, the menu bar was being animated in.  Not a show-stopper, admittedly, but it did look a bit wierd.

The problem was how to discriminate between taps on the background, which should trigger the toolbar display, and taps on the hotspots which shouldn’t.  The answer lay in changing the process completely, and relying on the javascript to call out to an Objective-C function in the app.

Going the other way - in other words, calling javascript from Objective-C - isn’t difficult.  You use UIWebView‘s stringByEvaluatingJavaScriptFromString: function and pass in the function that you want to call:

[myWebView stringByEvaluatingJavaScriptFromString:@"myJavascriptFunction()"];

Doing it the other way - getting the javascript to trigger an Objective-C method - isn’t so obvious.  There are various more-or-less hideous solutions at the other end of a Google search that involve categories and binding systems, all of which seemed like a lot of work for something relatively simple.  The answer turns out to be much less difficult, albeit somewhat hacky.

UIWebView comes with a delegate method of webView:shouldStartLoadWithRequest:navigationType: that’s called every time a request is made to load another URL from within the webView itself. By returning YES, you allow the webView to go ahead and load the new content, while returning NO stops the process dead in its tracks.  The URL being requested appears in the request parameter:

- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType

The revised solution was to remove the gesture recognizer altogether, and use the javascript to implement a method called panoTap() that is called every time the panorama is tapped on the background.  This tries to load a fake URL:

// Called when the pano is tapped OTHER than to open or close a hotspot
function panoTap() {
    // Hey, Objective-C, do something!!
    window.location = "toolbar://pano/tapped:param1:param2:param3";

The webView:shouldStartLoadWithRequest:navigationType: examines the URL sent over, and reacts to the fake URL:

-(BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType {
    NSURL *url = request.URL;
    if ([[url scheme] isEqualToString:@"toolbar"]) {
        [self didTapOnView];
    return YES;
    return YES;

If the “toolbar” scheme is received, the toolbar positioning method is triggered - otherwise the method just returns YES to allow the “load” to complete (nothing will happen as far as the webView is concerned, because that URL doesn’t actually exist.)  This also allows the initial load of the webView’s HTML to take place when the webView is first instantiated.

By breaking down the URL parameter and extracting the parameters, you could handle more complex scenarios by passing values out of the javascript and reacting accordingly.