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 -
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 -
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) -
And you need to modify viewDidLoad function to set delegate of WebView to ViewController instance -
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 -
Viewcontroller.h -
And ViewController.m -
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
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
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]; }
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
#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
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 iOS, JavaScript, Objective-C. Bookmark the permalink.
Thanks a lot.
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.
http://stackoverflow.com/questions/19048933/catching-javascript-asynchronous-calls-with-objective-c
Does your framework solve this issue?
setTimeout(function(){
calliOSFunction("getName", ["Ram"], "onSuccess", "onError");
},10);
setTimeout(function(){
calliOSFunction("sayHello", ["Ram"], "onSuccess", "onError");
},10);
setTimeout(function(){
calliOSFunction("getName", ["Ram"], "onSuccess", "onError");
},10);