Calling Objective-C code from JavaScript in iOS applications

In the last post I described how to Create iOS Application with HTML User Interface . In this post I am going to describe how to access Objective-C code from JavaScript running in the WebView. It is not as easy as it is in Android. There is no direct API in UIWebView to call native code, unlike in Android which provides WebView.addJavascriptInterface function.

However, there is a work-around. We can intercept each URL before it is being loaded in the WebView. So to call the native code, we will have to construct a URL and pass method name and argument as part of the URL. We then set this URL to window.location in the JavaScript and intercept it in our ViewController.
However most examples I have seen (including PhoneGap) create an instance of iframe and set its source to the URL –

function openCustomURLinIFrame(src)
{
    var rootElm = document.documentElement;
    var newFrameElm = document.createElement("IFRAME");
    newFrameElm.setAttribute("src",src);
    rootElm.appendChild(newFrameElm);
    //remove the frame now
    newFrameElm.parentNode.removeChild(newFrameElm);
}

Following JavaScript function creates a JSON string from function name and arguments and then calls openCustomURLinIFrame.

function calliOSFunction(functionName, args, successCallback, errorCallback)
{
    var url = "js2ios://";

    var callInfo = {};
    callInfo.functionname = functionName;
    if (successCallback)
    {
        callInfo.success = successCallback;
    }
    if (errorCallback)
    {
        callInfo.error = errorCallback;
    }
    if (args)
    {
        callInfo.args = args;
    }

    url += JSON.stringify(callInfo)

    openCustomURLinIFrame(url);
}

Note that I have created the url with custom protocol ‘js2ios’. This is just a marker protocol which we will intercept and process in the ViewController of iOS application. Since the code to call native method is asynchronous and does not return the result of the execution, we will have to pass success and error callback functions. These callback functions would be called from the native code.

calliOSFunction("sayHello", ["Ram"], "onSuccess", "onError");

function onSuccess (ret)
{
    if (ret)
    {
        var obj = JSON.parse(ret);
        document.write(obj.result);
    }
}

function onError (ret)
{
    if (ret)
    {
        var obj = JSON.parse(ret);
        document.write(obj.error);
    }
}

In the above code I am passing callback functions as string, which is not the best way. But we will see how to fix this later.
Let’s now see what we need to implement in Objective-C to execute ‘sayHello’ method, when called from JavaScript.

The ViewController in our application should implement UIWebViewDelegate protocol –

@interface RKViewController : UIViewController<UIWebViewDelegate>
{
    IBOutlet UIWebView *webView;
}
@end

Then we implement shouldStartLoadWithRequest method of UIWebViewDelegate protocol (interface). This function would be called before loading each url in the WebView. If you return true from this function, the WebView will load the URL, else it would not. So if the url contins our custom protocol, then we would process it and return false, so that WebView does not attempt to load it.
Following code implements shouldStartLoadWithRequest and also other functions to process our custom protocol (this code goes in ViewController.m) –

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

    NSURL *url = [request URL];
    NSString *urlStr = url.absoluteString;

    return [self processURL:urlStr];

}

- (BOOL) processURL:(NSString *) url
{
    NSString *urlStr = [NSString stringWithString:url];

    NSString *protocolPrefix = @"js2ios://";

    //process only our custom protocol
    if ([[urlStr lowercaseString] hasPrefix:protocolPrefix])
    {
        //strip protocol from the URL. We will get input to call a native method
        urlStr = [urlStr substringFromIndex:protocolPrefix.length];

        //Decode the url string
        urlStr = [urlStr stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding];

        NSError *jsonError;

        //parse JSON input in the URL
        NSDictionary *callInfo = [NSJSONSerialization
                                  JSONObjectWithData:[urlStr dataUsingEncoding:NSUTF8StringEncoding]
                                  options:kNilOptions
                                  error:&jsonError];

        //check if there was error in parsing JSON input
        if (jsonError != nil)
        {
            NSLog(@"Error parsing JSON for the url %@",url);
            return NO;
        }

        //Get function name. It is a required input
        NSString *functionName = [callInfo objectForKey:@"functionname"];
        if (functionName == nil)
        {
            NSLog(@"Missing function name");
            return NO;
        }

        NSString *successCallback = [callInfo objectForKey:@"success"];
        NSString *errorCallback = [callInfo objectForKey:@"error"];
        NSArray *argsArray = [callInfo objectForKey:@"args"];

        [self callNativeFunction:functionName withArgs:argsArray onSuccess:successCallback onError:errorCallback];

        //Do not load this url in the WebView
        return NO;

    }

    return YES;
}

- (void) callNativeFunction:(NSString *) name withArgs:(NSArray *) args onSuccess:(NSString *) successCallback onError:(NSString *) errorCallback
{
    //We only know how to process sayHello
    if ([name compare:@"sayHello" options:NSCaseInsensitiveSearch] == NSOrderedSame)
    {
        if (args.count > 0)
        {
            NSString *resultStr = [NSString stringWithFormat:@"Hello %@ !", [args objectAtIndex:0]];

            [self callSuccessCallback:successCallback withRetValue:resultStr forFunction:name];
        }
        else
        {
            NSString *resultStr = [NSString stringWithFormat:@"Error calling function %@. Error : Missing argument", name];
            [self callErrorCallback:errorCallback withMessage:resultStr];
        }
    }
    else
    {
        //Unknown function called from JavaScript
        NSString *resultStr = [NSString stringWithFormat:@"Cannot process function %@. Function not found", name];
        [self callErrorCallback:errorCallback withMessage:resultStr];

    }
}

-(void) callErrorCallback:(NSString *) name withMessage:(NSString *) msg
{
    if (name != nil)
    {
        //call error handler

        NSMutableDictionary *resultDict = [[NSMutableDictionary alloc] init];
        [resultDict setObject:msg forKey:@"error"];
        [self callJSFunction:name withArgs:resultDict];
    }
    else
    {
        NSLog(@"%@",msg);
    }

}

-(void) callSuccessCallback:(NSString *) name withRetValue:(id) retValue forFunction:(NSString *) funcName
{
    if (name != nil)
    {
        //call succes handler

        NSMutableDictionary *resultDict = [[NSMutableDictionary alloc] init];
        [resultDict setObject:retValue forKey:@"result"];
        [self callJSFunction:name withArgs:resultDict];
    }
    else
    {
        NSLog(@"Result of function %@ = %@", funcName,retValue);
    }

}

-(void) callJSFunction:(NSString *) name withArgs:(NSMutableDictionary *) args
{
    NSError *jsonError;

    NSData *jsonData = [NSJSONSerialization dataWithJSONObject:args options:0 error:&jsonError];

    if (jsonError != nil)
    {
        //call error callback function here
        NSLog(@"Error creating JSON from the response  : %@",[jsonError localizedDescription]);
        return;
    }

    //initWithBytes:length:encoding
    NSString *jsonStr = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];

    NSLog(@"jsonStr = %@", jsonStr);

    if (jsonStr == nil)
    {
        NSLog(@"jsonStr is null. count = %d", [args count]);
    }

    [webView stringByEvaluatingJavaScriptFromString:[NSString stringWithFormat:@"%@('%@');",name,jsonStr]];
}

 

And you need to modify viewDidLoad function to set delegate of WebView to ViewController instance –

- (void)viewDidLoad
{
    [super viewDidLoad];

    NSURL *url = [NSURL fileURLWithPath:[[NSBundle mainBundle] pathForResource:@"index" ofType:@"html" inDirectory:@"wwwroot"]];

    NSURLRequest *req = [NSURLRequest requestWithURL:url];

    //Set delegate for WebView
    [webView setDelegate:self];

    [webView loadRequest:req];

}

When you run the application, ‘sayHello’ function would be called from the JavaScript in index.html and executed by Objective-C code above. If the function is successfully executed then success callback function of JavaScript would be called.
Notice that in the above code stringByEvaluatingJavaScriptFromString method of UIWebView class is called to execute JavaScript code from Objective-C code.
Now change the function to be called in index.html to ‘sayHello1’ and run the application. You will see error message ‘Function not found’ in the application. This is displayed by calling JavaScript error callback function.

The above example shows how to call native code from JavaScript and vice versa. But the solution is not very elegant. I wanted to implement a generic solution which would make this process very easy. Also  I wanted to fix the issue of having to pass callback function as string (function name) to JavaScript function ‘calliOSFunction’. I also wanted to support inline function (closures) for success and callback functions.
So I created a small framework that abstracts many of the common functionality and provides a parent class for ViewController and a JavaScript file to be included in the index.html –

WebViewController.h
WebViewController.m
WebViewInterface.h
iosBridge.js

WebViewController and WebViewInterface files should go in the source folder of your project, along with other .h and .m files. iosBridge.js should go in the same folder as index.html – in our example it is in wwwroot. To see newly added class files in the project navigator in Xcode, CTRL+Click on the source folder (same name as project name) and select ‘Add Files to …” option.

Note that you still have to create WebView in .xib file and connect to webView outlet in WebViewController.h as described in my earlier post.

Using this framework, the index.html file looks as follows –

<script src="iosBridge.js" type="text/javascript"></script>

<script type="text/javascript">

    function onSuccess (ret)
    {
        if (ret)
        {
            document.write(ret.result);
        }
    }

    calliOSFunction("sayHello",["Ram"],onSuccess,
        function(ret)
        {
            if (ret)
            document.write("Error executing native function : <br>" + ret.message);
        }
    );

</script>

We use calliOSFunction to call native code. You can either pass function name or inline function for success and error callbacks.

Viewcontroller.h –

#import "WebViewController.h"

@interface RKViewController : WebViewController

@end

And ViewController.m –

#import "RKViewController.h"

@interface RKViewController ()

@end

@implementation RKViewController

- (NSString *) getInitialPageName
{
    return @"index.html";
}

- (id) processFunctionFromJS:(NSString *) name withArgs:(NSArray*) args error:(NSError **) error
{

    if ([name compare:@"sayHello" options:NSCaseInsensitiveSearch] == NSOrderedSame)
    {
        if (args.count > 0)
        {
            return [NSString stringWithFormat:@"Hello %@ !", [args objectAtIndex:0]];
        }
        else
        {
            NSString *resultStr = [NSString stringWithFormat:@"Missing argument in function %@", name];
            [self createError:error withCode:-1 withMessage:resultStr];
            return nil;
        }
    }
    else
    {
        NSString *resultStr = [NSString stringWithFormat:@"Function '%@' not found", name];
        [self createError:error withCode:-1 withMessage:resultStr];
        return nil;
    }
}

@end

You need to override two methods in the ViewController –

1. – (NSString *) getInitialPageName
This function should return page name to be loaded in the WebView. The framework looks for this file in wwwroot folder. You can also return a url from this function, starting with http or https.

2. – (id) processFunctionFromJS:(NSString *) name withArgs:(NSArray*) args error:(NSError **) error
You would write code to process function call from JavaScript here.

I believe this framework would make calling Objective-C from JavaScript a bit more easier.

-Ram Kulkarni

UPDATE : See ‘Framework for interacting with embedded WebView in iOS application‘ for the updated framework.

25 Replies to “Calling Objective-C code from JavaScript in iOS applications”

  1. Nice work, man! I am having trouble when sending the text content from a input text field to objective-c code when the text includes accented characters (like résumé). How can we overcome this?

  2. I am getting the problem when sending html data (body.innerHTML) to native method. Native method is unable to prepare the callInfo object. Can you help me. Thanks in advance.

  3. I resolved by adding condition only for that method call in native.
    Any Idea how to enable the keyboard by using javascript. Actually element.focus() is not working in the iOS devices, but it is working in the safari browser.

    1. I have not specifically handled concurrency in my framework. But when I tried to make different asynchronous calls from JS I did not see any issue. This is the code I tried with my framework –
      setTimeout(function(){
      calliOSFunction("getName", ["Ram"], "onSuccess", "onError");
      },10);
      setTimeout(function(){
      calliOSFunction("sayHello", ["Ram"], "onSuccess", "onError");
      },10);
      setTimeout(function(){
      calliOSFunction("getName", ["Ram"], "onSuccess", "onError");
      },10);

  4. Hi ram kulkarni, the first thank you so much for useful post, but when i apply your code to my project I alway meet this error at line :
    if (jsonError != nil) {
    NSLog(@”Error parsing JSON for the url %@”,url);
    return NO;
    }
    this error not happen if I run your project .
    I’m waiting your solution :((

    1. It is rather difficult to tell why the same code would not work for you. See what is the error message by replacing NSLog line with this –
      NSLog(@”Error parsing JSON for the url %@, Error Message : %@”,url, [jsonError localizedDescription]);

      1. this is error content . [NSURL localizedDescription]: unrecognized selector sent to instance 0x99951a0
        2014-02-14 16:17:22.312MySdk[509:a0b] *** WebKit discarded an uncaught exception in the webView:decidePolicyForNavigationAction:request:frame:decisionListener: delegate: -[NSURL localizedDescription]: unrecognized selector sent to instance 0x99951a0

        …………….
        I’m created a customView, webview is member on it .

  5. Hi Ram, I am trying to change the url passed to on click method by calling a javascript function pmfkeyCheck2() as below :

    Forgot Password

    so the javascript function looks like this:
    function pmfkeyCheck2(url){

    var pKey=document.getElementById(“pmfkey”).value;
    //var forgot = document.getElementById(“forgot”);

    if(pKey==””){

    alert(“Please enter your PMF Key”);
    forgot.href=”#”;
    }
    else{
    forgot.href=url+pKey;
    }

    Here’s the url that thats is passed as a parameter is passed from iOS settings bundle. Below is the code that i am usifn in iOS to pass the url to pmfkeyCheck2 which is then called by on click event:

    – (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType;
    {

    NSString *settingBunndleUrl = [webView stringByEvaluatingJavaScriptFromString:@”window.location.href”];

    if( [settingBunndleUrl rangeOfString:@”forgotquestions”].location != NSNotFound)
    {
    NSString * jsCallBack = [NSString stringWithFormat:@”pmfkeyCheck2(‘%@’)”,[Global instance].getForgotPasswordURL];
    NSLog(@”url value %@”, [Global instance].getForgotPasswordURL);
    [_webView stringByEvaluatingJavaScriptFromString:jsCallBack];
    }
    }

    But the url seems to be null..Could you please help me figuring out where I am going wrong? Im new to iOS and as well java scripting. Your help is appreciated.

  6. Hi,

    I am new to native function call.In My Javascript on success I would like to navigate to my own html.

    In the above example where should I write sayHello method.I do not have any controllers in my code

  7. Hi, Thanks for this framework.
    I am trying to add it to my Ionic project however I don’t arrive. I am beginner and I have several problems, I don’t really know what I should keep and what I need to modify from my ionic project because I need both working.
    Can you help me ?
    For example I have a MainViewController.xib in my project which is the view that I see when I launch the app. But I to see the view which is in main.storyboard, and I don t know and see how to do it.
    In addition, I don t know what I should modify in my AppDelegate.h and AppDelegate.m.
    I modify MainViewController.h and MainViewController.m as describe above but I don t know if there is stuff that the ionic parts need.
    Thanks for your help 😉
    Best regards

    1. I solve the majority of my problem. Now the front is working I can see my view created under Ionic and the one in Objective C but the back is not working. Do you have an idea of some mistakes that I made ?
      Thanks a lot 😉

Leave a Reply to Deane Saunders-Stowe Cancel reply