Techno Barje

UIWebView Secrets - Part3 - How to Properly Call ObjectiveC From Javascript

Let’s change the subject: this time no more talks about memory but always on UIWebView component. When we use this component for something else than just displaying webpages, like building UI with HTML, Javascript, … We often want to call Javascript functions from objective C and the opposite.


Call Javascript function from Objective-C:

The first move is easily done with the following piece of code:

  // In your Javascript files:
    function myJavascriptFunction () {
    
      // Do whatever your want!
    
    }
  
  // -----------------------------------
  
  // And in your Objective-C code:
  // Call Javascript function from Objective-C:
    [webview stringByEvaluatingJavaScriptFromString:@"myJavascriptFunction()"];


Call Objective-C function from Javascript:

But calling objective-c from a Javascript function is not easy as Iphone SDK doesn’t offer any native way to do this! So we have to use any king of hack to do this …
The most known, used and buggy practice is to register a UIWebViewDelegate on your web view and « catch-and-immediatly-cancel » a location change done in javascript.

(a very extremely plenty much advised practice!)

  // In Objective-C
  - someFunctionOnInit {
    
    webView = [[UIWebView alloc] init];
    // Register the UIWebViewDelegate in order to shouldStartLoadWithRequest to be called (next function)
    webView.delegate = self;  
    
  }
  
  // This function is called on all location change :
  - (BOOL)webView:(UIWebView *)webView2 
          shouldStartLoadWithRequest:(NSURLRequest *)request 
          navigationType:(UIWebViewNavigationType)navigationType {
    
    // Intercept custom location change, URL begins with "js-call:"
    if ([[[request URL] absoluteString] hasPrefix:@"js-call:"]) {
      
      // Extract the selector name from the URL
      NSArray *components = [requestString componentsSeparatedByString:@":"];
      NSString *function = [components objectAtIndex:1];
      
      // Call the given selector
      [self performSelector:NSSelectorFromString(functionName)];
      
      // Cancel the location change
      return NO;
    }
    
    // Accept this location change
    return YES;
    
  }
  
  - (void)myObjectiveCFunction {
    
    // Do whatever you want!
   
  }

  // -----------------------------------
  
  // Now in your javascript simply do this to call your objective-c function:
  // /!\ But for those who just read title and code, take care, this is a buggy practice /!\\n  window.location = "js-call:myObjectiveCFunction";



What’s wrong with UIWebViewDelegate, shouldStartLoadWithRequest and location change ?

There is weird but apprehensible bugs with this practice:
a lot of javascript/html stuff get broken when we cancel a location change:

  • All setInterval and setTimeout immediatly stop on location change
  • Every innerHTML won’t work after a canceled location change!
  • You may get other really weird bugs, really hard to diagnose …

Sample application highlighting these bugs

Key files of this example:

  • MyWebview.m: Objective-c part, that inherit from UIWebView. Set the UIWebViewDelegate and catch requests in shouldStartLoadWithRequest selector.
  • NativeBridge.js: Tiny javascript library in order to change the location and offer a way to send arguments and receive a response.
  • webview-script.js: Test case script, that highlight these bugs.

In webview-script.js: InnerHTML stop working whereas textContent continues to …

  document.getElementById("count").innerHTML = i;
  document.getElementById("count2").textContent = i;

But we can’t charge Apple on this bug. I mean we try to load another location in the document we are working on! The webview component may start doing stuff before the delegate call, which cancel the load …
We have to find alternative way to communicate with the native code!


Better way to call Objective-C

The only thing we have to change is in Javascript code. Instead of changing the document location, we create an IFrame and set its location to a value that trigger the shouldStartLoadWithRequest method.
And voilà!

  var iframe = document.createElement("IFRAME");
  iframe.setAttribute("src", "js-frame:myObjectiveCFunction";
  document.documentElement.appendChild(iframe);
  iframe.parentNode.removeChild(iframe);
  iframe = null;

Here is another sample application, with exactly the same structures and test file.
But this time you are going to see innerHTML and setTimeout working! Again, this demo contains a library (NativeBridge.js) that allow to send arguments to native code and get back a result in javascript asynchronously, with a callback function.

Best practice example!



Free Objective-C<->Javascript library

Finally I provide the communication library under LGPL licence so it can ease your work on iphone platform! As I know that it’s really not easy ;-)

The code is full of comment, so you may easily use and tweak it!

Github repo