How to call Objective-C from Javascript? - iphone

I have a WebView, and I want to call a view in Objective-C from JavaScript. Does someone know how I can do this?
I have this code in my ViewController:
- (BOOL)webView:(UIWebView *)webView2
shouldStartLoadWithRequest:(NSURLRequest *)request
navigationType:(UIWebViewNavigationType)navigationType {
NSString *requestString = [[request URL] absoluteString];
NSArray *components = [requestString componentsSeparatedByString:#":"];
if ([components count] > 1 &&
[(NSString *)[components objectAtIndex:0] isEqualToString:#"myapp"]) {
if([(NSString *)[components objectAtIndex:1] isEqualToString:#"myfunction"])
{
NSLog([components objectAtIndex:2]); [[Airship shared] displayStoreFront]; //<- This is the code to open the Store
NSLog([components objectAtIndex:3]); // param2
// Call your method in Objective-C method using the above...
}
return NO;
}
return YES; // Return YES to make sure regular navigation works as expected.
}
And in Javascript:
function store(event)
{
document.location = "myapp:" + "myfunction:" + param1 + ":" + param2;
}
But nothing happens.

The standard workaround for UIWebView is to set a UIWebViewDelegate, and implement the method webView:shouldStartLoadWithRequest:navigationType:. In your JavaScript code, navigate to some fake URL that encodes the information you want to pass to your app, like, say:
window.location = "fake://myApp/something_happened:param1:param2:param3";
In your delegate method, look for these fake URLs, extract the information you need, take whatever action is appropriate, and return NO to cancel the navigation. It's probably best if you defer any lengthy processing using some flavor of performSelector.

The window.location method of calling objective c from JS isn't recommended. One example of problems: if you make two immediate consecutive calls one is ignored (since you can't change location too quickly) - try it yourself..
I recommend the following alternative approach:
function execute(url)
{
var iframe = document.createElement("IFRAME");
iframe.setAttribute("src", url);
document.documentElement.appendChild(iframe);
iframe.parentNode.removeChild(iframe);
iframe = null;
}
You call the execute function repeatedly and since each call executes in its own iframe, they should not be ignored when called quickly.
Credits to this guy.

Obliviux,
Your code seems to be perfect.
The reason for the problem is that you must have missed to map the delegate.
Either
Connect the delegate of the webView to the file owner in the .xib file
or
Use webView.delegate = self;
in your viewDidLoad.
Thanks

Like people said here, you have to use the method webView:shouldStartLoadWithRequest:navigationType: from the UIWebviewDelegate.
This api http://code.google.com/p/jsbridge-to-cocoa/ does it for you. It is very lightweight. You can pass images, strings and arrays from javascript to objective-C.

I had an issue with this approach: I wanted to send several messages to the iphone device, but it seemed that they were "overlaped" as they could not process all of them sequentially.
Example: when executing this code:
window.location = "app://action/foo";
window.location = "app://action/bar";
The action foo was never executed.
What I had to do was the following:
waitingForMessage = false;
function MsgProcessed(){
waitingForMessage = false;
}
function SyncLaunchURL(url){
if (waitingForMessage){
setTimeout(function(){SyncLaunchURL(url)},100);
}else{
window.location = url
waitingForMessage = true;
}
}
SyncLaunchURL("app://action/foo");
SyncLaunchURL("app://action/bar");
With this approach, the iphone has to call MsgProcessed() after processing the call. This way works for me, and maybe helps someone with the same problem!

Assuming you're doing an app, you can look at how PhoneGap implements that (or even use it). It's a library that supports back-and-forth communication between JS and OBJ-C. There are other libraries and solutions, as well.
If you're talking about a web app (something the user gets to from Mobile Safari), you can't get to Objective-C from there.

Check this one - understanding XMLHttpRequest responses using this (or other javascript) functions?, it's using objective C to call ajax js function, and get the response after it's done, you know the trick is that webview will be triggered when you change the location in javascript, so you can check the location to know its your javascript call or the real request.

Although this is a very old question now, it keeps getting returned by Google and there is a good answer now: the WebScripting informal protocol. It allows you to expose an objective C object to Javascript.
http://developer.apple.com/library/safari/documentation/appleapplications/Conceptual/SafariJSProgTopics/Tasks/ObjCFromJavaScript.html#//apple_ref/doc/uid/30001215-BBCBFJCD

Related

JavaScript synchronous native communication to WKWebView

Is synchronous communication between JavaScript and Swift/Obj-C native code possible using the WKWebView?
These are the approaches I have tried and have failed.
Approach 1: Using script handlers
WKWebView's new way of receiving JS messages is by using the delegate method userContentController:didReceiveScriptMessage: which is invoked from JS by window.webkit.messageHandlers.myMsgHandler.postMessage('What's the meaning of life, native code?')
The problem with this approach is that during execution of the native delegate method, JS execution is not blocked, so we can't return a value by immediately invoking webView.evaluateJavaScript("something = 42", completionHandler: nil).
Example (JavaScript)
var something;
function getSomething() {
window.webkit.messageHandlers.myMsgHandler.postMessage("What's the meaning of life, native code?"); // Execution NOT blocking here :(
return something;
}
getSomething(); // Returns undefined
Example (Swift)
func userContentController(userContentController: WKUserContentController, didReceiveScriptMessage message: WKScriptMessage) {
webView.evaluateJavaScript("something = 42", completionHandler: nil)
}
Approach 2: Using a custom URL scheme
In JS, redirecting using window.location = "js://webView?hello=world" invokes the native WKNavigationDelegate methods, where the URL query parameters can be extracted. However, unlike the UIWebView, the delegate method is not blocking the JS execution, so immediately invoking evaluateJavaScript to pass a value back to the JS doesn't work here either.
Example (JavaScript)
var something;
function getSomething() {
window.location = "js://webView?question=meaning" // Execution NOT blocking here either :(
return something;
}
getSomething(); // Returns undefined
Example (Swift)
func webView(webView: WKWebView, decidePolicyForNavigationAction navigationAction: WKNavigationAction, decisionHandler decisionHandler: (WKNavigationActionPolicy) -> Void) {
webView.evaluateJavaScript("something = 42", completionHandler: nil)
decisionHandler(WKNavigationActionPolicy.Allow)
}
Approach 3: Using a custom URL scheme and an IFRAME
This approach only differs in the way that window.location is assigned. Instead of assigning it directly, the src attribute of an empty iframe is used.
Example (JavaScript)
var something;
function getSomething() {
var iframe = document.createElement("IFRAME");
iframe.setAttribute("src", "js://webView?hello=world");
document.documentElement.appendChild(iframe); // Execution NOT blocking here either :(
iframe.parentNode.removeChild(iframe);
iframe = null;
return something;
}
getSomething();
This nonetheless, is not a solution either, it invokes the same native method as Approach 2, which is not synchronous.
Appendix: How to achieve this with the old UIWebView
Example (JavaScript)
var something;
function getSomething() {
// window.location = "js://webView?question=meaning" // Execution is NOT blocking if you use this.
// Execution IS BLOCKING if you use this.
var iframe = document.createElement("IFRAME");
iframe.setAttribute("src", "js://webView?question=meaning");
document.documentElement.appendChild(iframe);
iframe.parentNode.removeChild(iframe);
iframe = null;
return something;
}
getSomething(); // Returns 42
Example (Swift)
func webView(webView: UIWebView, shouldStartLoadWithRequest request: NSURLRequest, navigationType: UIWebViewNavigationType) -> Bool {
webView.stringByEvaluatingJavaScriptFromString("something = 42")
}
No I don't believe it is possible due to the multi-process architecture of WKWebView. WKWebView runs in the same process as your application but it communicates with WebKit which runs in its own process (Introducing the Modern WebKit API). The JavaScript code will be running in the WebKit process. So essentially you are asking to have synchronous communication between two different processes which goes against their design.
I found a hack for doing synchronous communication but haven't tried it yet: https://stackoverflow.com/a/49474323/2870783
Edit: Basically you can use the the JS prompt() to carry your payload from the js side to the native side. In the native WKWebView will have to intercept the prompt call and decide if it is a normal call or if it is a jsbridge call. Then you can return your result as a callback to the prompt call. Because the prompt call is implemented in such a way that it waits for user input your javascript-native communication will be synchronous. The downside is that you can only communicate trough strings.
I also investigated this issue, and failed as you. To workaround, you have to pass a JavaScript function as a callback. Native function needs to evaluate the callback function to return result. Actually, this is the JavaScript way, because JavaScript never wait. Blocking JavaScript thread may cause ANR, it's very bad.
I have created a project named XWebView which can establish a bridge between native and JavaScript. It offers binding styled API for calling native from JavaScript and vice versa. There is a sample app.
It's possible to synchronously wait for the result of evaluateJavaScript by polling the current RunLoop's acceptInput method. What this does is allow your UI thread to respond to input while you wait for the the Javascript to finish.
Please read warnings before you blindly paste this into your code
//
// WKWebView.swift
//
// Created by Andrew Rondeau on 7/18/21.
//
import Cocoa
import WebKit
extension WKWebView {
func evaluateJavaScript(_ javaScriptString: String) throws -> Any? {
var result: Any? = nil
var error: Error? = nil
var waiting = true
self.evaluateJavaScript(javaScriptString) { (r, e) in
result = r
error = e
waiting = false
}
while waiting {
RunLoop.current.acceptInput(forMode: RunLoop.Mode.default, before: Date.distantFuture)
}
if let error = error {
throw error
}
return result
}
}
What happens is that, while the Javascript is executing, the thread calls RunLoop.current.acceptInput until waiting is false. This allows your application's UI to be responsive.
Some warnings:
Buttons, ect, on your UI will still respond. If you don't want someone to push a button while your Javascript is running, you should probably disable interacting with your UI. (This is especially the case if you're calling out to another server in Javascript.)
The multi-process nature of calling evaluateJavaScript may be slower than you expect. If you're calling code that is "instant," things may still slow down if you make repeated calls into Javascript in a tight loop.
I've only tested this on the Main UI thread. I don't know how this code will work on a background thread. If there are problems, investigate using a NSCondition.
I've only tested this on macOS. I do not know if this works on iOS.
I was facing a similar issue, i resolved it by storing promise callbacks.
The js that you load in your web view via WKUserContentController::addUserScript
var webClient = {
id: 1,
handlers: {},
};
webClient.onMessageReceive = (handle, error, data) => {
if (error && webClient.handlers[handle].reject) {
webClient.handlers[handle].reject(data);
} else if (webClient.handlers[handle].resolve){
webClient.handlers[handle].resolve(data);
}
delete webClient.handlers[handle];
};
webClient.sendMessage = (data) => {
return new Promise((resolve, reject) => {
const handle = 'm' + webClient.id++;
webClient.handlers[handle] = { resolve, reject };
window.webkit.messageHandlers.<message_handler_name>.postMessage({data: data, id: handle});
});
}
Perform Js Request like
webClient.sendMessage(<request_data>).then((response) => {
...
}).catch((reason) => {
...
});
Receive request in userContentController :didReceiveScriptMessage
Call evaluateJavaScript with webClient.onMessageReceive(handle, error, response_data).

How to use plugin for phonegap

I am new in phonegap development . I have created one plugin class for iphone which contains some method for encryption. I want to use that .h and .m file to encrypt my data and get the data from my class file in my html page. But i don't have any idea how to call that class function in my javascript file as a result of my page. Please any one have idea how to call any class file and its method in javascript for iphone application then help me .I am waiting for reply.
See the link that Paul answered. For a quick run down, there are a couple ways to do this, but mine usually end up something like this.
Javascript:
PhoneGap.exec("NameOfObjectiveCFile.nameofMethod","parameter1", "parameter2");
Objective C:
-(void)nameOfMethod:(NSMutableArray*)paramArray withDict: (NSMutableDictionary*) options
{
//Do your stuff
//Access your params you sent in javascript by doing the following
NSString *parameter1 = [paramArray objectAtIndex:0];
//Send stuff back to Javascript using a callback function
NSString *jsCallBack = [NSString stringWithFormat:#"nameofJavascriptFinishedMethod(%#)",parameterToSendBack];
[self.webview stringByEvaluatingJavaScriptFromString:jsCallBack];
}
Back to Javascript:
function nameofJavascriptFinishedMethod(parameterFromObjectiveC)
{
//Do stuff with information from objective C
}
Also remember you need to register your plugin in your Phonegap.plist
Hope this helps, good luck.
Update: If you want your html to send information to your plugin, i'd use an html form or button or trigger an action that will call your javascript function (The first one above) and pass your variables gathered from your fields. See this link for form basics.
Update 2
1)Create new phonegap project in xcode and build to get www folder
2)Add your existing Encryption objective C files to project
3)Create new objective C class, call it EncryptPlugin (See step 5 for next)
4)Edit PhoneGap.plist file
a)add a new entry under Plugins
b)name it EncryptPlugin String EncryptPlugin
5)Header file for EncryptPlugin should look like this
#import <Foundation/Foundation.h>
#import <UIKit/UIkit.h>
#ifdef PHONEGAP_FRAMEWORK
#import <PhoneGap/PGPlugin.h>
#else
#import "PGPlugin.h"
#endif
#interface EncryptPlugin : PGPlugin {
}
-(void) useMyEncryptionFiles:(NSMutableArray*)paramArray withDict:(NSMutableDictionary*)options;
#end
6)Implementation file should look like this
#import "EncryptPlugin.h"
#import "EncryptPacket.h"
#implementation EncryptPlugin
//phonegap magic -- basically ignore
-(PGPlugin*) initWithWebView:(UIWebView*)theWebView
{
self = (PdfCreator*)[super initWithWebView:theWebView];
return self;
}
//The method that your javascript is going to call
-(void) useMyEncryptionFiles:(NSMutableArray*)paramArray withDict:(NSMutableDictionary*)options
{
//This gives you your string
NSString *stringFromJavascript = [paramArray objectAtIndex:0];
EncryptPacket *ep = [[EncryptPacket alloc] init];
NSString *encryptedString = [ep encryptRequest:stringFromJavascript];
NSLog(#"encryptedString = %#",encryptedString);
}
#end
7)In your html javascript call the function using something like this
p1.html
<head><script type="text/javascript" src="encryptdata.js"></script></head>
<body>
<input type="password" name="confirmPassword" id="confirmPassword" value="" />
<input type="button" value="FetchR" onclick="fetchRelation()"/>
</body>
encryptdata.js
function fetchRelation()
{
var getvalue=document.getElementById('confirmPassword').value;
//what is next step....... to send the data to the plugin class
PhoneGap.exec("EncryptPlugin.useMyEncrptionFiles",getValue);
}
This should get you started. If you want to send stuff back to your html javascript then use the function I specified above:
NSString *jsCallBack = [NSStringstringWithFormat:#"nameofJavascriptFinishedMethod(%#)",parameterToSendBack];
[self.webview stringByEvaluatingJavaScriptFromString:jsCallBack];
Hopefully this helps, you should be able to figure it out with these instructions. They probably aren't perfect, but I didn't have time to put it all together and compile it using your code. Good luck.
See How to Create a PhoneGap Plugin for iOS

PhoneGap ChildBrowser Executing JavaScript

I wonder if this is possible to execute JavaScript inside phonegap childbrowser window so we can manipulate websites under phonegap app?
Looking at the big picture as one can create a function in Objective-C which executes that JS into childbrowser (modifying childbrowser.m and childbrowser.h files) and creating JS wrapper of it so one can call JS function to execute JS inside childbrowser.
I want you to modify ChildBrowser for me to have that functionality so I shouldn't lost doing it. At least give me initial steps.
Alright I just tried and it worked in a single go. That was amazing! I just modified ChildBrowser plugin of PhoneGap and it worked.
UPDATED
I finally got few minutes to update the answer for those who will encounter the same issue.
ChildBrowserCommand.h
- (void) jsExec:(NSMutableArray*)arguments withDict:(NSMutableDictionary*)options;
ChildBrowserCommand.m
- (void) jsExec:(NSMutableArray*)arguments withDict:(NSMutableDictionary*)options; {
[childBrowser executeJS:(NSString *)[arguments objectAtIndex:0]];
}
ChildBrowserViewController.h
- (void)executeJS:(NSString *)js;
ChildBrowserViewController.m
- (void) executeJS:(NSString *)js {
[webView stringByEvaluatingJavaScriptFromString:js];
}
ChildBrowser.js
/* MIT licensed */
// (c) 2010 Jesse MacFadyen, Nitobi
function ChildBrowser()
{
}
// Callback when the location of the page changes
// called from native
ChildBrowser._onLocationChange = function(newLoc)
{
window.plugins.childBrowser.onLocationChange(newLoc);
}
// Callback when the user chooses the 'Done' button
// called from native
ChildBrowser._onClose = function()
{
window.plugins.childBrowser.onClose();
}
// Callback when the user chooses the 'open in Safari' button
// called from native
ChildBrowser._onOpenExternal = function()
{
window.plugins.childBrowser.onOpenExternal();
}
// Pages loaded into the ChildBrowser can execute callback scripts, so be careful to
// check location, and make sure it is a location you trust.
// Warning ... don't exec arbitrary code, it's risky and could cause your app to fail.
// called from native
ChildBrowser._onJSCallback = function(js, loc)
{
// Not Implemented
window.plugins.childBrowser.onJSCallback(js, loc);
}
/* The interface that you will use to access functionality */
// Show a webpage, will result in a callback to onLocationChange
ChildBrowser.prototype.showWebPage = function(loc)
{
PhoneGap.exec("ChildBrowserCommand.showWebPage",loc);
}
// close the browser, will NOT result in close callback
ChildBrowser.prototype.close = function()
{
PhoneGap.exec("ChildBrowserCommand.close");
}
// Not Implemented
ChildBrowser.prototype.jsExec = function(jsString)
{
// Not Implemented!!
PhoneGap.exec("ChildBrowserCommand.jsExec", jsString);
}
// Note: this plugin does NOT install itself, call this method some time after deviceready to install it
// it will be returned, and also available globally from window.plugins.childBrowser
ChildBrowser.install = function()
{
if(!window.plugins)
{
window.plugins = {};
}
window.plugins.childBrowser = new ChildBrowser();
return window.plugins.childBrowser;
}
My global variable.
var CB = null;
On my DeviceReady event.
CB = ChildBrowser.install();
if (CB != null) {
CB.onLocationChange = onCBLocationChanged;
}
I can execute any JS into webpage using.
CB.jsExec("alert('I am from ChildBrowser!');");
I hope my contribution to this will bring smile on your face.

Proper way to declare two parameters in a Objective-C method?

This might be a ridiculous question but I can't find it asked yet here already.
Have a protocol delegate method defined:
- (void)myAddViewController:(MyAddViewController *)myAddViewController
loadGPS:(BOOL)gps loadCamera:(BOOL)camera;
which basically is to determine whether the GPS system is to be loaded or the camera should be loaded.
I call this method via:
[self.delegate myAddViewController:self loadGPS:YES loadCamera:NO];
// or alternatively
[self.delegate myAddViewController:self loadGPS:NO loadCamera:YES];
Inside my implemented method in the delegate we have:
- (void)myAddViewController:(MyAddViewController *)myAddViewController loadGPS:(BOOL)gps loadCamera:(BOOL)camera {
.... .... ...
if (gps) {
......
}
if (camera) {
// camera is ALWAYS nil and never seems to be set?!
.....
}
So why when I call the delegate method with Camera: YES is the camera var always nil? It seems like it is never recognizing my second var yet it doesn't mind compiling? :)
It seems like a waste to pass in two mutually exclusive boolean values to a method.
Perhaps you'd be better with two delegate methods:
[self.delegate myAddGPSViewController:self];
// or alternatively
[self.delegate myAddCameraViewController:self];
because when you're firing the delegate methods, you'll already know whether you want GPS or Camera anyway.
It might have,
declaring 'camera' variable again,
can you paste your method
- (void)myAddViewController:(MyAddViewController *)myAddViewController loadGPS:(BOOL)gps loadCamera:(BOOL)camera {
\here?

Triggering shouldStartLoadWithRequest with multiple window.location.href calls

Im trying to pass multiple things from a webpage inside a UIWebView back to my iPhone app via the shouldStartLoadWithRequest method of the UIWebView.
Basically my webpage calls window.location.href = "command://foo=bar" and i am able to intercept that in my app no problem. Now if i create a loop and do multiple window.location.href calls at once, then shouldStartLoadWithRequest only appears to get called on once and the call it gets is the very last firing of window.location.href at the end of the loop.
The same thing happens with the webview for Android, only the last window.location.href gets processed.
iFrame = document.createElement("IFRAME");
iFrame.setAttribute("src", "command://foo=bar");
document.body.appendChild(iFrame);
iFrame.parentNode.removeChild(iFrame);
iFrame = null;
So this creates an iframe, sets its source to a command im trying to pass to the app, then as soon as its appended to the body shouldStartLoadWithRequest gets called, then we remove the iframe from the body, and set it to null to free up the memory.
I also tested this on an Android webview using shouldOverrideUrlLoading and it also worked properly!
I struck this problem also and here is my solution that works for me.
All my JavaScript functions use this function __js2oc(msg) to pass data
and events to Objective-C via shouldStartLoadWithRequest:
P.S. replace "command:" with your "appname:" trigger you use.
/* iPhone JS2Objective-C bridge interface */
var __js2oc_wait = 300; // min delay between calls in milliseconds
var __prev_t = 0;
function __js2oc(m) {
// It's a VERY NARROW Bridge so traffic must be throttled
var __now = new Date();
var __curr_t = __now.getTime();
var __diff_t = __curr_t - __prev_t;
if (__diff_t > __js2oc_wait) {
__prev_t = __curr_t;
window.location.href = "command:" + m;
} else {
__prev_t = __curr_t + __js2oc_wait - __diff_t;
setTimeout( function() {
window.location.href = "command:" + m;
}, (__js2oc_wait - __diff_t));
}
}
No, iframe's url changing won't trigger shouldOverrideUrlLoading, at least no in Android 2.2.