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.
Really nice
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?
Solved by using the right encoding in stringByReplacingPercentEscapesUsingEncoding:NSISOLatin1StringEncoding (processURL function)
Good work.
Thanks a lot.
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.
It is difficult to tell what could be the problem, but try encoding the data in JS before calling native method.
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.
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?
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);
Thanks
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 :((
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]);
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 .
Sorry, I have never seen this error. But a quick search on “unrecognized selector sent to instance” showed up many solutions.
Obrigado, Ram Kulkarni.
Me ajudou bastante…
Vlw, abraços.
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.
Thank you guys, It is helped a lot to me. It resolved my major issue in the project. Thank you once again
Very useful blog for me . Thanks for sharing your good experience. Great work. Thanks a lot.!! recording app
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
For those who are involved in the process of localizing their iOS application, I recommend trying a collaborative translation management platform like https://poeditor.com/ that can simplify the entire workflow.
Thanks!! I Like this code use for development hybrid app.
Thanks, this blog is vary helpful for me. And your instructions are also clear and step by step.
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
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 😉
Wonderful blog! Thanks for sharing this with us. This is really helpful.
very nice blog….thanks for sharing
Hi Ram;
I cannot use this code ios version 12.2, this code run ios 8.4 or 9.0 . How can solve this problem.
Hi Ram,
I am trying to execute javasript code from iOS App(Swift). The piece of code I am executing actually utilises promises and returns a value from the “then” callback.
Now I have noticed that the method always returns “undefined” as it appears that the javascript execution is not being completed and just returns the value.
Do you know a work around to this issue? I have a common business logic written using JavaScript which I am trying ti access in both iOS and Android. Both iOS and Android apps are not hybrid, they’re made using Swift and Java.
Could you please suggest anything on this?
Thanks!
Thank you for sharing this post here!!!!
Interesting Things.
Took me time to read all the comments, but I really enjoyed the article. It proved to be Very helpful to me and I am sure to all the commenters here! It’s always nice when you can not only be informed, but also entertained!
Thanks for sharing an good informative blog with more details. I like the helpful info you provide in your articles. I have saved it for my future reference. Keep it up the good work.