How do I call an Objective-C method from Javascript in UIWebView? - javascript

I'm developing a native iPhone app using Phonegap, so everything is done in HTML and JS. I am using the Flurry SDK for analytics and want to use the
[FlurryAPI logEvent:#"EVENT_NAME"];
method to track events. Is there a way to do this in Javascript? So when tracking a link I would imagine using something like
<a onClick="flurryTrackEvent("Click_Rainbows")" href="#Rainbows">Rainbows</a>
<a onClick="flurryTrackEvent("Click_Unicorns")" href="#Unicorns">Unicorns</a>
"FlurryAPI.h" has the following:
#interface FlurryAPI : NSObject {
}
+ (void)startSession:(NSString *)apiKey;
+ (void)logEvent:(NSString *)eventName;
+ (void)logEvent:(NSString *)eventName withParameters:(NSDictionary *)parameters;
+ (void)logError:(NSString *)errorID message:(NSString *)message exception:(NSException *)exception;
+ (void)setUserID:(NSString *)userID;
+ (void)setEventLoggingEnabled:(BOOL)value;
+ (void)setServerURL:(NSString *)url;
+ (void)setSessionReportsOnCloseEnabled:(BOOL)sendSessionReportsOnClose;
#end
I'm only interested in the logEvent method(s). If it's not clear by now, I'm comfortable with JS but a recovering Obj-C noob. I've read the Apple docs but the examples described there are all for newly declared methods and I imagine this could be simpler to implement because the Obj-C method(s) are already defined.
Thank you in advance for any input.

One way to do this is to setup a delegate on the UIWebView which has the shouldStartLoadEvent. Inside that event, you check what URL the UIWebView is trying to navigate to. Now to communicate from JavaScript to Objective-C, you need to specify your own custom anchors which will trigger different actions. For example, to log something, you might decide to use the anchor "#FAPI_LogEvent_Click_Rainbows".
In JavaScript, you could have methods defined like such:
function flurryTrackEvent(text) {
window.location.href = 'FAPI_LogEvent' + text;
}
function flurrySetUserID(userID) {
window.location.href = 'FAPI_SetUserID' + userID;
}
Next, in Objective-C, you would implement the shouldStartLoadEvent and "capture" these href navigations, and tell the browser not to load them. You will need to split the string up yourself and call the appropriate function. Here's some code:
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType () {
NSString *theAnchor = [[request URL] fragment];
if ([theAnchor hasPrefix:#"FAPI_LogEvent"]) {
NSString *textToLog = [theAnchor substringFromIndex:[#"FAPI_LogEvent" length]];
[FlurryAPI logEvent:textToLog];
return NO; // prevent the UIWebView from navigating to this anchor
} else if ([theAnchor hasPrefix:#"FAPI_SetUserID"]) {
NSString *userID = [theAnchor substringFromIndex:[#"FAPI_SetUserID" length]];
[FlurryAPI setUserID:userID];
return NO; // prevent the UIWebView from navigating to this anchor
}
}
The fact that the events are already defined in Objective-C doesn't really help much since you need to implement your own routing behavior to call the appropriate Objective-C method. The only way you could take advantage of the fact that the methods are already defined in Objective-C and avoid hard coding the routing logic, would be using #selectors or similar dynamic function calling which is available in Objective-C. However, this is much more complicated to implement and probably presents a security risk. I would recommend implementing the routing logic like is shown in the code above.

PhoneGap has functionality for adding native plugins, to add a Flurry log event plugin for iOS I would do something like this:
Add a PGFlurry PhoneGap plugin class:
PGFlurry.h
#import <PhoneGap/PGPlugin.h>
#interface PGFlurry : PGPlugin
- (void)logEvent:(NSArray*)arguments withDict:(NSDictionary*)options;
#end
PGFlurry.m
#import "PGFlurry.h"
#import "FlurryAPI.h"
#implementation PGFlurry
// if you init Flurry somewhere else you can remove this method
- (PGPlugin*) initWithWebView:(UIWebView*)theWebView {
self = [super init];
if (self == nil) {
return nil;
}
// init and start Flurry
[FlurryAPI startSession:#"API key"];
return self;
}
- (void)logEvent:(NSArray*)arguments withDict:(NSDictionary*)options {
[FlurryAPI logEvent:[arguments objectAtIndex:0]];
}
#end
Add a JavaScript plugin helper to the www folder:
Flurry.js
PhoneGap.addConstructor(function() {
if(!window.plugins) {
window.plugins = {};
}
window.plugins.flurry = {
logEvent: function(name) {
return PhoneGap.exec("PGFlurry.logEvent", name);
}
}
});
Add the plugin to PhoneGap.plist by adding a key/value pair with both the key and value being "PGFlurry" to the "plugins" dictionary.
Now you should be able to use it like this:
<!DOCTYPE html>
<html>
<head>
<script src="phonegap.js" type="text/javascript"></script>
<script src="flurry.js" type="text/javascript"></script>
<script type="text/javascript">
document.addEventListener("deviceready", function() {
window.plugins.flurry.logEvent("Testing");
}, false);
</script>
</head>
<body>
</body>
</html>

You can find the Phonegap Flurry Plugin written by me at
https://github.com/designerkamal/Phonegap-Flurry-Plugin

Don't use their objective-c library, use their js library and you won't have to worry about objective-c. :)

Related

Call JavaScript from Objective-C in AppDelegate

Is there any way to call device token from Objective-C?
I just created a cordova ios project since I only have basic knowledge in Objective-C. It will automatically generate AppDelegate.m class
#implementation AppDelegate
- (BOOL)application:(UIApplication*)application didFinishLaunchingWithOptions:(NSDictionary*)launchOptions
{
self.viewController = [[MainViewController alloc] init];
NSString * jsCallBack = [NSString stringWithFormat:#"myFunction()"];
[webView stringByEvaluatingJavaScriptFromString:jsCallBack];
return [super application:application didFinishLaunchingWithOptions:launchOptions];
}
#end
How can a javascript function be called from index.html file, that loaded in inappbrowser
<!DOCTYPE html>
<html>
<head>
<title>Device Ready Example</title>
<script type="text/javascript" charset="utf-8" src="cordova.js"></script>
<script type="text/javascript" charset="utf-8">
// Wait for device API libraries to load
//
myFunction()
{
alert('called');
}
function onLoad() {
document.addEventListener("deviceready", onDeviceReady, false);
}
// device APIs are available
//
function onDeviceReady() {
// Now safe to use device APIs
}
</script>
</head>
<body onload="onLoad()">
</body>
</html>
First of all:
make sure that your webview has already loaded the HTML page (in case you also need to modify certain UI elements in the DOM, it should maybe also be rendered already. The loading itself is performed asynchronously. That means you cannot implement it in one single process, but have to wait until it is ready for manipulation.
As you call the function through injection in the application's start method, I think the webview is not yet prepared.
Instead use the Delegate's approach for iOS applications:
https://developer.apple.com/reference/uikit/uiwebviewdelegate
The UIWebViewDelegate informs you about certain state(-changes). e.g:
Webiew starts to load content
A Link was clicked
Webview finished loading
So I would recommend extending your AppDelegate (or create a new delegate) with the following delegate property:
#property (nonatomic) UIWebViewDelegate *webDelegate;
Set the delegate of the webView accordingly:
- (BOOL)application:(UIApplication*)application didFinishLaunchingWithOptions:(NSDictionary*)launchOptions
{
self.viewController = [[MainViewController alloc] init];
// I just used your very own code for this, I think you will have a separate property for this webView ?
webView.delegate = self;
return [super application:application didFinishLaunchingWithOptions:launchOptions];
}
And also implement one of the webview delegate methods (in this case, when a webview finished loading:
#implementation AppDelegate
- (BOOL)application:(UIApplication*)application didFinishLaunchingWithOptions:(NSDictionary*)launchOptions
{
...
}
- (void)webViewDidFinishLoad:(UIWebView *)webView
{
NSString * jsCallBack = [NSString stringWithFormat:#"myFunction()"];
[webView stringByEvaluatingJavaScriptFromString:jsCallBack];
}
#end
You can call javascript function from anywhere in objective C:
NSString *str=#"window['function_name'].apply(window,['param1','param2'])";
[webview evaluateJavaScript:str completionHandler:nil];

Executing Javascript from Objective C in Cordova Plugin

I'm using cordova 6.5.0 and I'm creating a plugin from which I would like to execute some javascript.
I have found in forums that from my webview I can use stringByEvaluatingJavascriptFromString but it can not be recognised as a valid method.
For instance in AppDelegate.m, just for testing, I've tried the following:
[self.viewController.webView stringByEvaluatingJavascriptFromString]:#"alert('Test')"];
but I receive the message "No visible #inteface for 'UIView' declares the selector 'stringByEvaluatingJavascriptFromString'".
I have created a class object named Utils.m
#implementation Utils: NSObject
id webview;
id delegate;
-(void) initialise:(id) wbview Delegate:(id) dlg{
webview = wbview;
delegate = dlg;
}
-(void) executeJavascript:(NSString *)str{
[delegate runInBackground:^{
[delegate stringByEvaluatingJavaScriptFromString:#"alert('test')"];
}];
}
And then from the cordova plugin from pluginInitialize I have
- (void)pluginInitialize{
/* Start pjsua app thread */
// [NSThread detachNewThreadSelector:#selector(pjsuaStart) toTarget:self withObject:nil];
utils = [[Utils alloc] init ];
[utils initialise:self.webView Delegate:self.commandDelegate];
[utils executeJavascript:#"alert('Test');"];
}
Although for some reason the stringByEvaluatingJavascriptFromString is considered a valid method, the application crashes...
Any suggestions?
Thanks
Symeon
Use evalJs not stringByEvaluatingJavaScriptFromString:
[delegate evalJs:#"alert('test')"];

iOS JavaScript bridge

I'm working on an app where I'm going to use both HTML5 in UIWebView and native iOS framework together. I know that I can implement communication between JavaScript and Objective-C. Are there any libraries that simplify implementing this communication? I know that there are several libraries to create native iOS apps in HTML5 and javascript (for example AppMobi, PhoneGap), but I'm not sure if there is a library to help create native iOS apps with heavy JavaScript usage. I need to:
Execute JS methods from Objective-C
Execute Objective-C methods from JS
Listen to native JS events from Objective-C (for example DOM ready event)
There are a few libraries, but I didn't used any of these in big projects, so you might want to try them out:
WebViewJavascriptBridge: https://github.com/marcuswestin/WebViewJavascriptBridge
GAJavaScript: https://github.com/newyankeecodeshop/GAJavaScript
—
However, I think it's something simple enough that you might give it a try yourself. I personally did exactly this when I needed to do that. You might also create a simple library that suits your needs.
1. Execute JS methods from Objective-C
This is really just one line of code.
NSString *returnvalue = [webView stringByEvaluatingJavaScriptFromString:#"your javascript code string here"];
More details on the official UIWebView Documentation.
2. Execute Objective-C methods from JS
This is unfortunately slightly more complex, because there isn't the same windowScriptObject property (and class) that exists on Mac OSX allowing complete communication between the two.
However, you can easily call from javascript custom-made URLs, like:
window.location = yourscheme://callfunction/parameter1/parameter2?parameter3=value
And intercept it from Objective-C with this:
- (BOOL)webView:(UIWebView*)webView shouldStartLoadWithRequest:(NSURLRequest*)request navigationType:(UIWebViewNavigationType)navigationType {
NSURL *URL = [request URL];
if ([[URL scheme] isEqualToString:#"yourscheme"]) {
// parse the rest of the URL object and execute functions
}
}
This is not as clean as it should be (or by using windowScriptObject) but it works.
3. Listen to native JS events from Objective-C (for example DOM ready event)
From the above explanation, you see that if you want to do that, you have to create some JavaScript code, attach it to the event you want to monitor and call the correct window.location call to be then intercepted.
Again, not clean as it should be, but it works.
The suggested method of calling objective c from JS in the accepted answer isn't recommended. One example of problems: if you make two immediate consecutive calls one is ignored (you can't change location too quickly).
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.
Update: This has changed in iOS 8. My answer applies to previous versions.
An alternative, that may get you rejected from the app store, is to use WebScriptObject.
These APIs are public on OSX but are not on iOS.
You need to define interfaces to the internal classes.
#interface WebScriptObject: NSObject
#end
#interface WebView
- (WebScriptObject *)windowScriptObject;
#end
#interface UIWebDocumentView: UIView
- (WebView *)webView;
#end
You need to define your object that's going to serve as your WebScriptObject
#interface WebScriptBridge: NSObject
- (void)someEvent: (uint64_t)foo :(NSString *)bar;
- (void)testfoo;
+ (BOOL)isKeyExcludedFromWebScript:(const char *)name;
+ (BOOL)isSelectorExcludedFromWebScript:(SEL)aSelector;
+ (WebScriptBridge*)getWebScriptBridge;
#end
static WebScriptBridge *gWebScriptBridge = nil;
#implementation WebScriptBridge
- (void)someEvent: (uint64_t)foo :(NSString *)bar
{
NSLog(bar);
}
-(void)testfoo {
NSLog(#"testfoo!");
}
+ (BOOL)isKeyExcludedFromWebScript:(const char *)name;
{
return NO;
}
+ (BOOL)isSelectorExcludedFromWebScript:(SEL)aSelector;
{
return NO;
}
+ (NSString *)webScriptNameForSelector:(SEL)sel
{
// Naming rules can be found at: https://developer.apple.com/library/mac/documentation/Cocoa/Reference/WebKit/Protocols/WebScripting_Protocol/Reference/Reference.html
if (sel == #selector(testfoo)) return #"testfoo";
if (sel == #selector(someEvent::)) return #"someEvent";
return nil;
}
+ (WebScriptBridge*)getWebScriptBridge {
if (gWebScriptBridge == nil)
gWebScriptBridge = [WebScriptBridge new];
return gWebScriptBridge;
}
#end
Now set that an instance to your UIWebView
if ([uiWebView.subviews count] > 0) {
UIView *scrollView = uiWebView.subviews[0];
for (UIView *childView in scrollView.subviews) {
if ([childView isKindOfClass:[UIWebDocumentView class]]) {
UIWebDocumentView *documentView = (UIWebDocumentView *)childView;
WebScriptObject *wso = documentView.webView.windowScriptObject;
[wso setValue:[WebScriptBridge getWebScriptBridge] forKey:#"yourBridge"];
}
}
}
Now inside of your javascript you can call:
yourBridge.someEvent(100, "hello");
yourBridge.testfoo();
In iOS8 you can look at WKWebView instead of UIWebView. This has the following class:
WKScriptMessageHandler: Provides a method for receiving messages from JavaScript running in a webpage.
This is possible with iOS7, checkout http://blog.bignerdranch.com/3784-javascriptcore-and-ios-7/
Your best bet is Appcelerators Titanium offering. They already have built a Obj-C javascript bridge using the V8 engine JavascriptCore engine used by webkit. It's also open source, so you'll be able to download it and tinker with the Obj-C as you like.
Have a look at the KirinJS project: Kirin JS which allows to use Javascript for the application logic and native UI adequate to the platform it runs on.
I created a library like WebViewJavascriptBridge, but it's more JQuery-like, has easier to setup and is easier to use. Doesn't rely on jQuery (though to its credit, had I known WebViewJavascriptBridge existed before writing this I may just have held back slightly before diving in). Let me know what you think! jockeyjs
If you are using WKWebView on iOS 8, take a look the XWebView which can automatically expose the native interface to javascript.

Possible to call an Objective-C method from Javascript in a Webview? [duplicate]

I'm developing a native iPhone app using Phonegap, so everything is done in HTML and JS. I am using the Flurry SDK for analytics and want to use the
[FlurryAPI logEvent:#"EVENT_NAME"];
method to track events. Is there a way to do this in Javascript? So when tracking a link I would imagine using something like
<a onClick="flurryTrackEvent("Click_Rainbows")" href="#Rainbows">Rainbows</a>
<a onClick="flurryTrackEvent("Click_Unicorns")" href="#Unicorns">Unicorns</a>
"FlurryAPI.h" has the following:
#interface FlurryAPI : NSObject {
}
+ (void)startSession:(NSString *)apiKey;
+ (void)logEvent:(NSString *)eventName;
+ (void)logEvent:(NSString *)eventName withParameters:(NSDictionary *)parameters;
+ (void)logError:(NSString *)errorID message:(NSString *)message exception:(NSException *)exception;
+ (void)setUserID:(NSString *)userID;
+ (void)setEventLoggingEnabled:(BOOL)value;
+ (void)setServerURL:(NSString *)url;
+ (void)setSessionReportsOnCloseEnabled:(BOOL)sendSessionReportsOnClose;
#end
I'm only interested in the logEvent method(s). If it's not clear by now, I'm comfortable with JS but a recovering Obj-C noob. I've read the Apple docs but the examples described there are all for newly declared methods and I imagine this could be simpler to implement because the Obj-C method(s) are already defined.
Thank you in advance for any input.
One way to do this is to setup a delegate on the UIWebView which has the shouldStartLoadEvent. Inside that event, you check what URL the UIWebView is trying to navigate to. Now to communicate from JavaScript to Objective-C, you need to specify your own custom anchors which will trigger different actions. For example, to log something, you might decide to use the anchor "#FAPI_LogEvent_Click_Rainbows".
In JavaScript, you could have methods defined like such:
function flurryTrackEvent(text) {
window.location.href = 'FAPI_LogEvent' + text;
}
function flurrySetUserID(userID) {
window.location.href = 'FAPI_SetUserID' + userID;
}
Next, in Objective-C, you would implement the shouldStartLoadEvent and "capture" these href navigations, and tell the browser not to load them. You will need to split the string up yourself and call the appropriate function. Here's some code:
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType () {
NSString *theAnchor = [[request URL] fragment];
if ([theAnchor hasPrefix:#"FAPI_LogEvent"]) {
NSString *textToLog = [theAnchor substringFromIndex:[#"FAPI_LogEvent" length]];
[FlurryAPI logEvent:textToLog];
return NO; // prevent the UIWebView from navigating to this anchor
} else if ([theAnchor hasPrefix:#"FAPI_SetUserID"]) {
NSString *userID = [theAnchor substringFromIndex:[#"FAPI_SetUserID" length]];
[FlurryAPI setUserID:userID];
return NO; // prevent the UIWebView from navigating to this anchor
}
}
The fact that the events are already defined in Objective-C doesn't really help much since you need to implement your own routing behavior to call the appropriate Objective-C method. The only way you could take advantage of the fact that the methods are already defined in Objective-C and avoid hard coding the routing logic, would be using #selectors or similar dynamic function calling which is available in Objective-C. However, this is much more complicated to implement and probably presents a security risk. I would recommend implementing the routing logic like is shown in the code above.
PhoneGap has functionality for adding native plugins, to add a Flurry log event plugin for iOS I would do something like this:
Add a PGFlurry PhoneGap plugin class:
PGFlurry.h
#import <PhoneGap/PGPlugin.h>
#interface PGFlurry : PGPlugin
- (void)logEvent:(NSArray*)arguments withDict:(NSDictionary*)options;
#end
PGFlurry.m
#import "PGFlurry.h"
#import "FlurryAPI.h"
#implementation PGFlurry
// if you init Flurry somewhere else you can remove this method
- (PGPlugin*) initWithWebView:(UIWebView*)theWebView {
self = [super init];
if (self == nil) {
return nil;
}
// init and start Flurry
[FlurryAPI startSession:#"API key"];
return self;
}
- (void)logEvent:(NSArray*)arguments withDict:(NSDictionary*)options {
[FlurryAPI logEvent:[arguments objectAtIndex:0]];
}
#end
Add a JavaScript plugin helper to the www folder:
Flurry.js
PhoneGap.addConstructor(function() {
if(!window.plugins) {
window.plugins = {};
}
window.plugins.flurry = {
logEvent: function(name) {
return PhoneGap.exec("PGFlurry.logEvent", name);
}
}
});
Add the plugin to PhoneGap.plist by adding a key/value pair with both the key and value being "PGFlurry" to the "plugins" dictionary.
Now you should be able to use it like this:
<!DOCTYPE html>
<html>
<head>
<script src="phonegap.js" type="text/javascript"></script>
<script src="flurry.js" type="text/javascript"></script>
<script type="text/javascript">
document.addEventListener("deviceready", function() {
window.plugins.flurry.logEvent("Testing");
}, false);
</script>
</head>
<body>
</body>
</html>
You can find the Phonegap Flurry Plugin written by me at
https://github.com/designerkamal/Phonegap-Flurry-Plugin
Don't use their objective-c library, use their js library and you won't have to worry about objective-c. :)

How to call an Objective-C method from Javascript in a Cocoa/WebKit app?

I have a Cocoa app that uses a WebView to display an HTML interface. How would I go about calling an Objective-C method from a Javascript function within the HTML interface?
This is documented at developer.apple.com.
Being rather green, Apple's documentation is pretty unusable for me, so I made a proof of concept of calling Objective C methods from javascript and vice versa in Cocoa, though the latter was much easier.
First make sure you have your webview as the setFrameLoadDelegate:
[testWinWebView setFrameLoadDelegate:self];
You need to tell the webview to watch for a specific object as soon as it's loaded:
- (void)webView:(WebView *)sender didClearWindowObject:(WebScriptObject *)windowScriptObject forFrame:(WebFrame *)frame {
//add the controller to the script environment
//the "ObjCConnector" object will now be available to JavaScript
[windowScriptObject setValue:self forKey:#"ObjCConnector"];
}
Then the business of the communication:
// a few methods to log activity
- (void)acceptJavaScriptFunctionOne:(NSString*) logText {
NSLog(#"acceptJavaScriptFunctionOne: %#",logText);
}
- (void)acceptJavaScriptFunctionTwo:(NSString*) logText {
NSLog(#"acceptJavaScriptFunctionTwo: %#",logText);
}
//this returns a nice name for the method in the JavaScript environment
+(NSString*)webScriptNameForSelector:(SEL)sel {
NSLog(#"%# received %# with sel='%#'", self, NSStringFromSelector(_cmd), NSStringFromSelector(sel));
if(sel == #selector(acceptJavaScriptFunctionOne:))
return #"functionOne"; // this is what you're sending in from JS to map to above line
if(sel == #selector(acceptJavaScriptFunctionTwo:))
return #"functionTwo"; // this is what you're sending in from JS to map to above line
return nil;
}
//this allows JavaScript to call the -logJavaScriptString: method
+ (BOOL)isSelectorExcludedFromWebScript:(SEL)sel {
NSLog(#"isSelectorExcludedFromWebScript: %#", NSStringFromSelector(sel));
if(sel == #selector(acceptJavaScriptFunctionOne:) ||
sel == #selector(acceptJavaScriptFunctionTwo:))
return NO;
return YES;
}
The key is that if you have multiple methods you'd like to call, you need to have them all excluded in the isSelectorExcludedFromWebScript method, and you need the javascript call to map out to the ObjC method in webScriptNameForSelector.
Full project proof of concept file:
https://github.com/bytestudios/JS-function-and-ObjC-method-connector
If you wanna do it in iPhone apps, you would need to do a trick with the UIWebViewDelegate method shouldStartLoadWithRequest:
This api http://code.google.com/p/jsbridge-to-cocoa/ does it for you. It is very lightweight.
I have a solution using NimbleKit. It can call Objective C functions from Javascript.

Categories

Resources