Calling Javascript in an iOS web app from Objective-C - javascript

I am developing a web app that is being displayed in a UIWebView. The app is loaded locally, i.e, not from a web server. I am communicating from Javascript to ObjC via the shouldStartLoadWithRequest: method in the UIWebViewDelegate protocol.
The last thing I need is to be able to call Javascript functions from ObjC without any page reloads. I hope this is possible.

Well, you can call
-[UIWebView stringByEvaluatingJavaScriptFromString:]
whenever you like, not just in response to a delegate method.

You can call any javascript function in your webview by simply using the stringByEvaluatingJavaScriptFromString method on your webview after you've loaded the webview:
[self.myWebView stringByEvaluatingJavaScriptFromString:#"myJavaScriptFunction(123.0)"];
You don't need to reload the webview to send this message (just don't release the webview before you're done).

Could't you just do this by doing a "loadRequest", and passing it an NSURL with contents like like:
javascript:myFunction("MyParameter");
It should call your function, but not reload the page.

One can use JavaScriptCore framework to run JavaScript code from Objective-C.
#import <JavaScriptCore/JavaScriptCore.h>
...
JSContext *context = [[JSContext alloc] init];
[context evaluateScript: #"function greet(name){ return 'Hello, ' + name; }"];
JSValue *function = context[#"greet"];
JSValue* result = [function callWithArguments:#[#"World"]];
[result toString]; // -> Hello, World
Here is a demo:
https://github.com/evgenyneu/ios-javascriptcore-demo

Related

Can External JS be executed on a loaded page (when complete) in iOS WkWebView

Can an external JS be injected into the Web View inside an iOS app (wkwebview to be specific), then executed?
Not adblock api for safari - but imagine simple JS/JQ that does the same. Find a class, changes the class.
Can this be done? If so - are there limitations to what types of Javascript can be executed in the WkWebview?
If you have a local JavaScript file that you want to load in and use. Similar to adding a
<script src="file.js" />
you can use this in swift
let root = NSBundle.mainBundle().resourceURL!
let JSURL = root.URLByAppendingPathComponent("file.js")
let JSString = try! String(contentsOfURL: JSURL)
let WKJS = WKUserScript(source: JSString, injectionTime: WKUserScriptInjectionTime.AtDocumentStart, forMainFameOnly: false)
webView.configuration.userContentController.addUserScript(WKJS)
For WKWebView, you have the following instance methods to execute Javascript. The JS to be execute has to be sent as a NSString to the methods.
Obj-C
- (void)evaluateJavaScript:(NSString *)javaScriptString
completionHandler:(void (^)(id,
NSError *error))completionHandler
Swift
func evaluateJavaScript(_ javaScriptString: String,
completionHandler completionHandler: ((AnyObject?,
NSError?) -> Void)?)
Reference: https://developer.apple.com/library/ios/documentation/WebKit/Reference/WKWebView_Ref/index.html#//apple_ref/occ/instm/WKWebView/evaluateJavaScript:completionHandler:

WKWebview's javascript call returns nil

I'm trying to migrate from using a UIWebview to a WKWebview, and I need to invoke various Javascript functions that live in the webview.
In the UIWebview I have no trouble doing this, but the WKWebview throws an WKErrorDomain Error 4 for any and all calls. I thought it might be a race condition where I was calling functions that didn't exist in the DOM yet, but I added a debug statement to make those same calls once the page loaded and they're not working. I even did a 'document.body.innerHTML' which returned nil when there's clearly content being displayed in the webview.
The twist is that when inspecting the web code via Safari Web Inspector, everything works well and all calls work.
Why and how could this be? I do initialize the WKWebView with a WKWebViewConfiguration so that it'll share cookies across different webviews, so there might be something there, but I'm stumped.
This is how i initialize the WKWebView
WKWebViewConfiguration* config = [[WKWebViewConfiguration alloc] init];
config.processPool = [[WKProcessPool alloc] init];
WKUserContentController* userContentController = WKUserContentController.new;
WKUserScript * cookieScript = [[WKUserScript alloc] initWithSource: #"document.cookie = 'cookie1=value1; domain=blah; path=/;';" injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:NO];
[userContentController addUserScript:cookieScript];
config.userContentController = userContentController;
WKWebView *webView = [[WKWebView alloc] initWithFrame:CGRectMake(0,0,50,50) configuration:config];
Here's a debugging statement where I log the document's innerHTML into the console log, which prints (nil) instead of the HTML.
NSLog(#"%#", [self.webView evaluateJavaScript:#"document.body.innerHTML" completionHandler:nil]);
WKWebView executes JS async unlike UIWebView; You cannot print like how you are doing..
Try this
[self.webView evaluateJavaScript:#"document.body.innerHTML" completionHandler:^(id result, NSError *error) {
NSLog(#"Error is %#",error);
NSLog(#"JS result %#" ,result);
}];

UIWebView JavaScript losing reference to iOS JSContext namespace (object)

I've been working on a proof of concept app that leverages two-way communication between Objective C (iOS 7) and JavaScript using the WebKit JavaScriptCore framework. I was finally able to get it working as expected, but have run into a situation where the UIWebView loses its reference to the iOS object that I've created via JSContext.
The app is a bit complex, here are the basics:
I'm running a web server on the iOS device (CocoaHTTPServer)
The UIWebView initially loads a remote URL, and is later redirected back to localhost as part of the app flow (think OAuth)
The HTML page that the app hosts (at localhost) has the JavaScript that should be talking to my iOS code
Here's the iOS side, my ViewController's .h:
#import <UIKit/UIKit.h>
#import <JavaScriptCore/JavaScriptCore.h>
// These methods will be exposed to JS
#protocol DemoJSExports <JSExport>
-(void)jsLog:(NSString*)msg;
#end
#interface Demo : UIViewController <UserInfoJSExports, UIWebViewDelegate>
#property (nonatomic, readwrite, strong) JSContext *js;
#property (strong, nonatomic) IBOutlet UIWebView *webView;
#end
And the pertinent parts of the ViewController's .m:
-(void)viewDidLoad {
[super viewDidLoad];
// Retrieve and initialize our JS context
NSLog(#"Initializing JavaScript context");
self.js = [self.webView valueForKeyPath:#"documentView.webView.mainFrame.javaScriptContext"];
// Provide an object for JS to access our exported methods by
self.js[#"ios"] = self;
// Additional UIWebView setup done here...
}
// Allow JavaScript to log to the Xcode console
-(void)jsLog(str) {
NSLog(#"JavaScript: %#", str);
}
Here is the (simplified for the sake of this question) HTML/JS side:
<html>
<head>
<title>Demo</title>
<script type="text/javascript">
function setContent(c, noLog){
with(document){
open();
write('<p>' + c + '</p>');
close();
}
// Write content to Xcode console
noLog || ios.jsLog(c);
}
</script>
</head>
<body onload="javascript:setContent('ios is: ' + typeof ios)">
</body>
</html>
Now, in almost all cases this works beautifully, I see ios is: object both in the UIWebView and in Xcode's console. Very cool. But in one particular scenario, 100% of the time, this fails after a certain number of redirects in the UIWebView, and once the above page finally loads it says:
ios is: undefined
...and the rest of the JS logic quits because the subsequent call to ios.jsLog in the setContent function results in an undefined object exception.
So finally my question: what could/can cause a JSContext to be lost? I dug through the "documentation" in the JavaScriptCore's .h files and found that the only way this is supposed to happen is if there are no more strong references to the JSContext, but in my case I have one of my own, so that doesn't seem right.
My only other hypothesis is that it has to do with the way in which I'm acquiring the JSContext reference:
self.js = [self.webView valueForKeyPath:#"documentView.webView.mainFrame.javaScriptContext"];
I'm aware that this may not be officially supported by Apple, although I did find at least one SO'er that said he had an Apple-approved app that used that very method.
EDIT
I should mention, I implemented UIWebViewDelegate to check the JSContext after each redirect in the UIWebView thusly:
-(void)webViewDidFinishLoad:(UIWebView *)view{
// Write to Xcode console via our JSContent - is it still valid?
[self.js evaluateScript:#"ios.jsLog('Can see JS from obj c');"];
}
This works in all cases, even when my web page finally loads and reports ios is: undefined the above method simultaneously writes Can see JS from obj c to the Xcode console. This would seem to indicate the JSContext is still valid, and that for some reason it's simply no longer visible from JS.
Apologies for the very long-winded question, there is so little documentation on this out there that I figured the more info I could provide, the better.
The page load can cause the WebView (and UIWebView which wraps WebView) to get a new JSContext.
If this was MacOS we were talking about, then as shown in the section on WebView in the 2013 WWDC introduction "Integrating JavaScript into Native Apps" session on Apple's developer network (https://developer.apple.com/videos/wwdc/2013/?id=615), you would need to implement a delegate for the frame load and initialise your JSContext variables in your implementation of the selector for webView:didCreateJavaScriptContext:forFrame:
In the case of IOS, you need to do this in webViewDidFinishLoad:
-(void)webViewDidFinishLoad:(UIWebView *)view{
self.js = [view valueForKeyPath:#"documentView.webView.mainFrame.javaScriptContext"]; // Undocumented access to UIWebView's JSContext
self.js[#"ios"] = self;
}
The previous JSContext is still available to Objective-C since you've kept a strong reference to it.
check this UIWebView JSContext
The key point is register a javascript object once JSContext changed. I use a runloop observer to check is there any network operation finished, once it finished, I'll get the changed JSContext, and register any object I want to it.
I didn't try if this work for iframe, if u have to register some objects in iframe, try this
NSArray *frames = [_web valueForKeyPath:#"documentView.webView.mainFrame.childFrames"];
[frames enumerateObjectsUsingBlock:^(id frame, NSUInteger idx, BOOL *stop) {
JSContext *context = [frame valueForKeyPath:#"javaScriptContext"];
context[#"Window"][#"prototype"][#"alert"] = ^(NSString *message) {
NSLog(#"%#", message);
};
}];

Communication between iOS's native app and webpage's javascript

I have a webpage loaded in a UIWebView, and a javascript function of the page needs to data from native iOs app, a NSString. How can a Js function access the data in native app?
Thanks,
lvreiny
You can execute JavaScript in your UIWebView from Obj-C. Simply call [webView stringByEvaluatingJavaScriptFromString:#"myJavaScript"];.
I could imagine a setup like this:
Webpage
<html>
<head>
<script type="text/javascript">
function callmeFromObjC(para1) {
// do something
alert(para1);
}
</script>
</head>
<body>
</body>
</html>
Objective-C
NSString *myParameter = #"myParameter";
[webView stringByEvaluatingJavaScriptFromString:[NSString stringWithFormat:#"callmeFromObjC('%#')", myParameter]];
With WebViewJavaScriptBridge you can achieve two way communication between javaScript and iOS.
Check this link below for WebViewJavaScriptBridge .
I used this bridge for one of my application for communication between iOS and JS and also vice versa.
https://github.com/marcuswestin/WebViewJavascriptBridge.
I created an iOS/JS library to help make this easier -- that is, communication in both directions using similar methods. You can check it out here: https://github.com/tcoulter/jockeyjs
Let the javascript load a custom URL, which your app intercepts. It than can parse it, prepare the data and pass it on to your webpage via stringByEvaluatingJavaScriptFromString:.
[webView loadHTMLString:#"<script src=\"filename.js\"></script>"
baseURL:[NSURL fileURLWithPath:[[NSBundle mainBundle] resourcePath]]];
NSString *result = [webView stringByEvaluatingJavaScriptFromString:#"function(parameter)"];
Give feedback to iOS
window.location = customprefix://function/parameter=value
- (BOOL)webView:(UIWebView*)webView shouldStartLoadWithRequest:(NSURLRequest*)request navigationType:(UIWebViewNavigationType)navigationType {
if ([[URL scheme] isEqualToString:#"customprefix"]) {
// handle function name and paramters
}
}
I also wrote a guide on how to call and handle different javascript functions from within iOS.
http://www.dplusmpage.com/2012/07/20/execute-javascript-on-ios/
Sample code for this is available here,you can check it....very usefull
http://ramkulkarni.com/blog/framework-for-interacting-with-embedded-webview-in-ios-application/

Can you call a javascript function from native code (not in a callback) using PhoneGap and iOS?

I'm hoping to be able to use PhoneGap for my app. I will have to build a custom protocol/plugin so that I can call Native methods from the Javascript. I know you can call a success function in the Javascript when the native code returns.
What I need to be able to do is call a javascript function from the native code. Basically the app will connect to an OSX companion app over local network and when the OSX app send data to the iOS app it is processed in an Objective C method, I need to be able to send the result into the PhoneGap/javascript and do something with it in the WebView.
Is this possible? I have only been able to find information about calling native from javascript not the other way around.
Thanks,
Thomas
Using the code from Answer below here:
MyPhoneGapPlugin.m
- (void)onSocket:(AsyncSocket *)sock didConnectToHost:(NSString *)host port:(UInt16)port {
NSLog(#"Connected To %#:%i.", host, port);
NSString* jsString = [NSString stringWithFormat:#"alert(connected to: %#);", host];
[theWebView stringByEvaluatingJavaScriptFromString:jsString];
[self readWithTag:2];
}
Giving me the error 'Unknown receiver 'theWebView' did you mean 'UIWebView'?
UPDATE: Found the answer: using the phonegap helper I can write something like this...
[super writeJavascript:#"alert('connected');"];
You can easily call JavaScript from native code with a UIWebView:
[webView stringByEvaluatingJavaScriptFromString:#"myJSFunction()"];
To use the result of a function somewhere as an arg to a JS function:
NSString *stringData = getStringData(); // however you get it
[webView stringByEvaluatingJavaScriptFromString:
[NSString stringWithFormat:#"myJSFunction(%#)", stringData]];
Found the PhoneGap helper to accomplish this... Write javascript to the webView using:
[super writeJavascript:#"alert('it works');"];
You should try this,
[webView stringByEvaluatingJavaScriptFromString:#"sendSelectedDate()"];
Will this work for you?
http://developer.apple.com/library/mac/#documentation/Cocoa/Conceptual/DisplayWebContent/Tasks/JavaScriptFromObjC.html
Taken from this page:
You can also call JavaScript functions with arguments. Assume that you have written a JavaScript function which looks like this:
function addImage(image, width, height) { ... }
Its purpose is to add an image to a web page. It is called with three arguments: image, the URL of the image; width, the screen width of the image; and height, the screen height of the image. You can call this method one of two ways from Objective-C. The first creates the array of arguments prior to using the WebScriptObject bridge:
id win = [webView windowScriptObject];
NSArray *args = [NSArray arrayWithObjects:
#"sample_graphic.jpg",
[NSNumber numberWithInt:320],
[NSNumber numberWithInt:240],
nil];
[win callWebScriptMethod:#"addImage"
withArguments:args];

Categories

Resources