UIWebView JavaScript to Objective-C communication

by

When building an iOS application there are times when the best solution for a particular view or screen is to build it as a webpage and then embed it into the app using a UIWebView. There are certain things a webview can do really well that are otherwise hard to replicate, and while not as sexy, web development can be much quicker than native iOS development. The specifics of when to use a webview and when to go native I won’t dive into, but if you decide to use a webview, the first major question you’ll probably wonder is how to communicate between the native app and the webview. For full integration into the app, being able to send messages from the JavaScript to Objective-C and back is critical, and luckily it’s quite easy.

Communicating from the app to a webview is ready out of the box. There is a method on UIWebview to evaluate a string as javascript in the global scope of your webview named stringByEvaluatingJavaScriptFromString:. So just create a global JS method (or many) and call them, and pass them paramters, whatever you want. (See http://bit.ly/L30KdP)

The more difficult task is to communicate from JS back to the iOS app. The best way to do this is to use the UIWebViewDelegate method webView:shouldStartLoadWithRequest:navigationType:, which allows that app to intercept load requests, and return whether the webview should proceed. The way to communicate to the app is to have your webview make fake requests, so to speak, and intercept them with this method. You’re welcome to do this however you want, but I’ll show my technique below.

Let’s get to the code. First we’ll define a global JS function that can take 2 strings and convert them to a key-value pair URL request.

var sendToApp = function(_key, _val) {
 var iframe = document.createElement("IFRAME"); 
 iframe.setAttribute("src", _key + ":##sendToApp##" + _val); 
 document.documentElement.appendChild(iframe); 
 iframe.parentNode.removeChild(iframe); 
 iframe = null; 
}; 

The above just takes a key-value pair, creates an iframe, attaches it to the document so it will try to load the URL, “key:##sendToApp##value”, and then throws it away.  Left alone, this method will just try to load iframes unsuccessfully, but it shouldn’t cause errors in your webview. We’re going to catch those requests anyway, so it doesn’t really matter, but it’s nice to know it won’t cause problems on its own.

The Objective-C code, to be put in the delegate of the webview, is going to catch these load requests, handle the key-value pair, and return ‘NO’.

I’m actually going to show you the implementation as I did it which is not directly in a UIWebViewDelegate. Instead, I created a category on NSObject which implements webView:shouldStartLoadWithRequest:navigationType: which sends the key-value pairs to another method, webviewMessageKey:value:. By using the informal protocol design, any class can import the category and then just implement webviewMessageKey:value: to receive key-value pairs from the Javscipt. I’ll explain it more in a second. Here’s the code:

- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType {
   NSString *requestString = [[[request URL] absoluteString] stringByReplacingPercentEscapesUsingEncoding: NSUTF8StringEncoding];
   NSArray *requestArray = [requestString componentsSeparatedByString:@":##sendToApp##"];

   if ([requestArray count] > 1){
      NSString *requestPrefix = [[requestArray objectAtIndex:0] lowercaseString];
      NSString *requestMssg = ([requestArray count] > 0) ? [requestArray objectAtIndex:1] : @"";
      [self webviewMessageKey:requestPrefix value:requestMssg];
      return NO;
   }
   else if (navigationType == UIWebViewNavigationTypeLinkClicked && [self shouldOpenLinksExternally]) {
      [[UIApplication sharedApplication] openURL:[request URL]];
      return NO;
   }
   return YES;
}
- (void)webviewMessageKey:(NSString *)key value:(NSString *)val {}
- (BOOL)shouldOpenLinksExternally {
   return YES;
}

The above code is the entire implementation of a category I call NSObject+SEWebViewJSListener (github link below). The webView:shouldStartLoadWithRequest:navigationType: method takes requests from the webview, if they have the structure ‘key:##sendToApp##value’, it sends them to webviewMessageKey:value: and returns ‘NO’. Otherwise, if it was a link clicked, it calls shouldOpenLinksExternally to determine if it should open the link in the webview or leave the app and use the device’s browser. I usually send the user to the browser as my embedded webviews aren’t meant for users to navigate through the internet.

That’s basically it.  If you have some class that wants to communicate with JavaScript in a webview, make it the webview’s delegate, import NSObject+SEWebViewJSListener, and add your own implementation of webviewMessageKey:value:, and you’ll automatically be able to send key-value pairs from anywhere in your JS directly to that method.  (Don’t forget to implement sendToApp in the JavaScript).

I know what you’re thinking, you’re thinking, “hey,that’s really cool”.  Well, it’s not 4 popped collars cool, but it’s pretty cool

If you want to jump in, a great place to start is to add logging from JavaScript to your iOS app. Add this in your JavaScript:

var log = function(_mssg){
   sendToApp("ios-log", _mssg);
};

And this to your Objective-C:

- (void)webviewMessageKey:(NSString *)key value:(NSString *)val {
   if ([key isEqualToString:@"ios-log"]) {
      NSLog(@"__js__>> %@", val);
   }
}

And then you can call log(“foobar”) in your JavaScript and you should see it show up in your iOS log file as “__js__>> foobar”.

You can get the code here: https://github.com/griddle/js-to-ios-grio-blog

 

8 Comments

  1. Nice solution, I have been using this in my UIWebView. I’m currently porting to WKWebView and noticed this is not working anymore. Did you already figure out how to use it in WKWebView? I will also keep on trying and when I have found a solution, I will post it here.
    Thanks

  2. Hey, thank you very much for this tutorial !
    I’ve run into a bit of a problem. Everything works fine if there is only function in the JS file.
    If I write another function and try to call it at some other time, it just wont happen.
    I even tried it by maintaining different JS files for each of the functions. NO Luck

Leave a Reply

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