Calling Objective-C code from JavaScript in iOS applications (from Ram's Blog)

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
{
    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 -


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
This entry was posted in iOS Programming, JavaScript and tagged , , . Bookmark the permalink.

10 Responses to Calling Objective-C code from JavaScript in iOS applications

  1. Pingback: Creating iOS Application with HTML User Interface | Ram's Blog
  2. Aashish says:
    Really nice
  3. Gustavo says:
    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?
  4. Gustavo says:
    Solved by using the right encoding in stringByReplacingPercentEscapesUsingEncoding:NSISOLatin1StringEncoding (processURL function)
  5. Siva krishna Dwaram says:
    Good work.
    Thanks a lot.
  6. Siva krishna Dwaram says:
    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.
  7. Siva krishna Dwaram says:
    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.
  8. Victor says:
    Hi! Thanks for the solution, could you help with this issue, please.
    http://stackoverflow.com/questions/19048933/catching-javascript-asynchronous-calls-with-objective-c
    Does your framework solve this issue?
    • Ram Kulkarni says:
      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);

이 블로그의 인기 게시물

둘 중 누군가 그녀를 죽였다, 범인 해설

[MAC OS X] mds_stores 100% CPU usage

tips more on unity ...