WKWebview's javascript call returns nil - javascript

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);
}];

Related

WKWebView evaluateJavaScript is not working for youtube embedded video url

I've loaded a youtube embedded video to WKWebView:
https://www.youtube.com/embed/amtuB-2wGeQ?playsinline=1&autoplay=1
Then after WKWebView didFinishNavigation event is fired, I call:
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
webView.evaluateJavaScript("document.querySelector('video').play()", completionHandler: { (result, error) in
if let r = result {
print(r)
}
if let e = error {
print(e)
}
})
}
However, the javascript command is not being executed, both error and result are nil.
When I execute the same javascript command in Chrome developer tool, it successfully played the video and paused the video by calling "play()" and "pause()".
document.querySelector('video').play()
document.querySelector('video').pause()
I don't know what happened inside WKWebView, any ideas?
Thanks!
OK, finally figured out why it is not working.
If you are also struggling with "why my javascript is not working in WKWebView", below is a neat way to figure out why:
1. Open Safari on your desktop, Preferences -> Advanced, enable "show developer menu in menu bar"
2. In the Safari menu, Develop -> simulator, connect your iPhone simulator
3. Now you can see the web page html and script in Safari, in the console input area, try run different javascript to see which one works.
4. Then call the one that works from "evaluateJavaScript"
The reason why it didn't work from my case is that the embedded web page was rendered differently between common web browser and WKWebView. I took the browser script as example which didn't work.
With the help of Safari debugger, you can see what's really going on inside WKWebView, which is really cool.
Hope it helps if you ever run into the same problem.
Thanks!
Use UIWebView instead of WKWebView and use this code for play youtube video.
#pragma mark - Embed Video
- (UIWebView *)embedVideoYoutubeWithURL:(NSString *)urlString andFrame:(CGRect)frame {
NSString *videoID = [self extractYoutubeVideoID:urlString];
NSString *embedHTML = #"\
<html><head>\
<style type=\"text/css\">\
body {\
background-color: transparent;\
color: white;\
}\
</style>\
</head><body style=\"margin:0\">\
<embed id=\"yt\" src=\"http://www.youtube.com/v/%#\" type=\"application/x-shockwave-flash\" \
width=\"%0.0f\" height=\"%0.0f\"></embed>\
</body></html>";
NSString *html = [NSString stringWithFormat:embedHTML, videoID, frame.size.width, frame.size.height];
UIWebView *videoWebView = [[UIWebView alloc] initWithFrame:frame];
[videoWebView loadHTMLString:html baseURL:nil];
return videoWebView;
}
/**
#see https://devforums.apple.com/message/705665#705665
extractYoutubeVideoID: works for the following URL formats:
www.youtube.com/v/VIDEOID
www.youtube.com?v=VIDEOID
www.youtube.com/watch?v=WHsHKzYOV2E&feature=youtu.be
www.youtube.com/watch?v=WHsHKzYOV2E
youtu.be/KFPtWedl7wg_U923
www.youtube.com/watch?feature=player_detailpage&v=WHsHKzYOV2E#t=31s
youtube.googleapis.com/v/WHsHKzYOV2E
*/
- (NSString *)extractYoutubeVideoID:(NSString *)urlYoutube {
NSString *regexString = #"(?<=v(=|/))([-a-zA-Z0-9_]+)|(?<=youtu.be/)([-a-zA-Z0-9_]+)";
NSError *error = NULL;
NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:regexString options:NSRegularExpressionCaseInsensitive error:&error];
NSRange rangeOfFirstMatch = [regex rangeOfFirstMatchInString:urlYoutube options:0 range:NSMakeRange(0, [urlYoutube length])];
if(!NSEqualRanges(rangeOfFirstMatch, NSMakeRange(NSNotFound, 0))) {
NSString *substringForFirstMatch = [urlYoutube substringWithRange:rangeOfFirstMatch];
return substringForFirstMatch;
}
return nil;
}
or use google's youtube-ios-player-helper pod

I would like to export the contents of ios web view as html

I am writing an app for ios to extract information from a webpage, however, the relevant pieces on the page are built by javascript. So when it is loaded by webview, the javascript is executed and the information displays no problem. If I try to load the page into a string by using the following method, the javascript is loaded, but not actually executed, therefore the string has no useful data in it.
NSData *urlData = [NSData dataWithContentsOfURL:[NSURL URLWithString:fullURL]];
NSString *responseString = [[NSString alloc] initWithData:urlData encoding:NSUTF8StringEncoding];
Is there another way besides loading the page into webview and exporting it from there? If not, how do you do that?
I'm not sure if there's another way outside of letting the UIWebView execute the JS and render the page, but if you do end up going this route, you could just grab the HTML of the whole page and pass that to the native end like so:
[dummyWebView stringByEvaluatingJavaScriptFromString:#"document.getElementsByTagName('html')[0].outerHTML;"];
Listening to the window.load event might be better to know when the page has finished going through all the JS
Good luck!
You set delegate to webView: self.webView.delegate = self; and implement UIWebViewDelegate:
- (void)webViewDidFinishLoad:(UIWebView *)webView
{
NSString *html = [webView stringByEvaluatingJavaScriptFromString:#"document.documentElement.outerHTML"];
NSLog(#"html1 = %#", html);
// or use
NSString *html2 = [webView stringByEvaluatingJavaScriptFromString:#"document.body.innerHTML"];
NSLog(#"html2 = %#", html2);
}

Capture (and prevent) alert() modal in UIWebView [duplicate]

<script language="javascript">
alert("Hell! UIWebView!");
</script>
I can see the alert message inside my UIWebView but can I handle this situation?
Update:
I'm loading a web-page into my UIWebView:
- (void)login {
NSString *requestText = [[NSString alloc] initWithFormat: #"%#?user=%#&password=%#", DEFAULT_URL, user.name, user.password]; // YES, I'm using GET request to send password :)
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:requestText]];
[webView loadRequest:request];
}
The target page contain a JS. If user name or password is incorrect this JS show alert.
I have not any access to its sources.
I want to handle it inside my UIWebViewDelegate.
A better solution to this problem is to create a Category for UIWebView for the method
webView:runJavaScriptAlertPanelWithMessage:initiatedByFrame:
So that you can handle the alert event in any way that you'd like. I did this because I don't like the default behavior of UIWebView when it puts the filename of the source in the UIAlertView title. The Category looks something like this,
#interface UIWebView (JavaScriptAlert)
- (void)webView:(UIWebView *)sender runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WebFrame *)frame;
#end
#implementation UIWebView (JavaScriptAlert)
- (void)webView:(UIWebView *)sender runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WebFrame *)frame {
UIAlertView* dialogue = [[UIAlertView alloc] initWithTitle:nil message:message delegate:nil cancelButtonTitle:#"Okay" otherButtonTitles:nil];
[dialogue show];
[dialogue autorelease];
}
#end
This seems to do it:
- (void)webViewDidFinishLoad:(UIWebView *)webView
{
JSContext *ctx = [webView valueForKeyPath:#"documentView.webView.mainFrame.javaScriptContext"];
ctx[#"window"][#"alert"] = ^(JSValue *message) {
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:#"JavaScript Alert" message:[message toString] delegate:nil cancelButtonTitle:#"OK" otherButtonTitles:nil];
[alert show];
};
}
Note: only tested on iOS 8.
If by "contain a flash" you mean the page you're loading into your web view has an Adobe Flash movie in it, you're out of luck, I'm afraid. Mobile Safari doesn't support Flash, and most likely never will.
In the general case, if you want JavaScript running in a web view to communicate with the native app hosting it, you can load fake URLs (for example: "myapp://alert?The+text+of+the+alert+goes+here."). That will trigger the webView:shouldStartLoadWithRequest:navigationType: delegate method. In that method, inspect the request, and if the URL being loaded is one of these internal communications, trigger the appropriate action in your app, and return NO.

Calling Javascript in an iOS web app from Objective-C

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

Detecting JavaScript errors in a UIWebView [duplicate]

I need to have my iPhone Objective-C code catch Javascript errors in a UIWebView. That includes uncaught exceptions, syntax errors when loading files, undefined variable references, etc.
This is for a development environment, so it doesn't need to be SDK-kosher. In fact, it only really needs to work on the simulator.
I've already found used some of the hidden WebKit tricks to e.g. expose Obj-C objects to JS and to intercept alert popups, but this one is still eluding me.
[NOTE: after posting this I did find one way using a debugging delegate. Is there a way with lower overhead, using the error console / web inspector?]
I have now found one way using the script debugger hooks in WebView (note, NOT UIWebView). I first had to subclass UIWebView and add a method like this:
- (void)webView:(id)webView windowScriptObjectAvailable:(id)newWindowScriptObject {
// save these goodies
windowScriptObject = newWindowScriptObject;
privateWebView = webView;
if (scriptDebuggingEnabled) {
[webView setScriptDebugDelegate:[[YourScriptDebugDelegate alloc] init]];
}
}
Next you should create a YourScriptDebugDelegate class that contains methods like these:
// in YourScriptDebugDelegate
- (void)webView:(WebView *)webView didParseSource:(NSString *)source
baseLineNumber:(unsigned)lineNumber
fromURL:(NSURL *)url
sourceId:(int)sid
forWebFrame:(WebFrame *)webFrame
{
NSLog(#"NSDD: called didParseSource: sid=%d, url=%#", sid, url);
}
// some source failed to parse
- (void)webView:(WebView *)webView failedToParseSource:(NSString *)source
baseLineNumber:(unsigned)lineNumber
fromURL:(NSURL *)url
withError:(NSError *)error
forWebFrame:(WebFrame *)webFrame
{
NSLog(#"NSDD: called failedToParseSource: url=%# line=%d error=%#\nsource=%#", url, lineNumber, error, source);
}
- (void)webView:(WebView *)webView exceptionWasRaised:(WebScriptCallFrame *)frame
sourceId:(int)sid
line:(int)lineno
forWebFrame:(WebFrame *)webFrame
{
NSLog(#"NSDD: exception: sid=%d line=%d function=%#, caller=%#, exception=%#",
sid, lineno, [frame functionName], [frame caller], [frame exception]);
}
There is probably a large runtime impact for this, as the debug delegate can also supply methods to be called for entering and exiting a stack frame, and for executing each line of code.
See http://www.koders.com/noncode/fid7DE7ECEB052C3531743728D41A233A951C79E0AE.aspx for the Objective-C++ definition of WebScriptDebugDelegate.
Those other methods:
// just entered a stack frame (i.e. called a function, or started global scope)
- (void)webView:(WebView *)webView didEnterCallFrame:(WebScriptCallFrame *)frame
sourceId:(int)sid
line:(int)lineno
forWebFrame:(WebFrame *)webFrame;
// about to execute some code
- (void)webView:(WebView *)webView willExecuteStatement:(WebScriptCallFrame *)frame
sourceId:(int)sid
line:(int)lineno
forWebFrame:(WebFrame *)webFrame;
// about to leave a stack frame (i.e. return from a function)
- (void)webView:(WebView *)webView willLeaveCallFrame:(WebScriptCallFrame *)frame
sourceId:(int)sid
line:(int)lineno
forWebFrame:(WebFrame *)webFrame;
Note that this is all hidden away in a private framework, so don't try to put this in code you submit to the App Store, and be prepared for some hackery to get it to work.
I created a nice little drop-in category that you can add to your project...
It is based on Robert Sanders solution. Kudos.
You can dowload it here:
UIWebView+Debug
This should make it a lot easier to debug you UIWebView :)
I used the great solution proposed from Robert Sanders: How can my iPhone Objective-C code get notified of Javascript errors in a UIWebView?
That hook for webkit works fine also on iPhone. Instead of standard UIWebView I allocated derived MyUIWebView. I needed also to define hidden classes inside MyWebScriptObjectDelegate.h:
#class WebView;
#class WebFrame;
#class WebScriptCallFrame;
Within the ios sdk 4.1 the function:
- (void)webView:(id)webView windowScriptObjectAvailable:(id)newWindowScriptObject
is deprecated and instead of it I used the function:
- (void)webView:(id)sender didClearWindowObject:(id)windowObject forFrame:(WebFrame*)frame
Also, I get some annoying warnings like "NSObject may not respond -windowScriptObject" because the class interface is hidden. I ignore them and it works nice.
One way that works during development if you have Safari v 6+ (I'm uncertain what iOS version you need) is to use the Safari development tools and hook into the UIWebView through it.
In Safari: Enable the Develop Menu (Preferences > Advanced > Show Develop menu in menu bar)
Plug your phone into the computer via the cable.
List item
Load up the app (either through xcode or just launch it) and go to the screen you want to debug.
Back in Safari, open the Develop menu, look for the name of your device in that menu (mine is called iPhone 5), should be right under User Agent.
Select it and you should see a drop down of the web views currently visible in your app.
If you have more than one webview on the screen you can try to tell them apart by rolling over the name of the app in the develop menu. The corresponding UIWebView will turn blue.
Select the name of the app, the develop window opens and you can inspect the console. You can even issue JS commands through it.
Straight Forward Way: Put this code on top of your controller/view that is using the UIWebView
#ifdef DEBUG
#interface DebugWebDelegate : NSObject
#end
#implementation DebugWebDelegate
#class WebView;
#class WebScriptCallFrame;
#class WebFrame;
- (void)webView:(WebView *)webView exceptionWasRaised:(WebScriptCallFrame *)frame
sourceId:(int)sid
line:(int)lineno
forWebFrame:(WebFrame *)webFrame
{
NSLog(#"NSDD: exception: sid=%d line=%d function=%#, caller=%#, exception=%#",
sid, lineno, [frame functionName], [frame caller], [frame exception]);
}
#end
#interface DebugWebView : UIWebView
id windowScriptObject;
id privateWebView;
#end
#implementation DebugWebView
- (void)webView:(id)sender didClearWindowObject:(id)windowObject forFrame:(WebFrame*)frame
{
[sender setScriptDebugDelegate:[[DebugWebDelegate alloc] init]];
}
#end
#endif
And then instantiate it like this:
#ifdef DEBUG
myWebview = [[DebugWebView alloc] initWithFrame:frame];
#else
myWebview = [[UIWebView alloc] initWithFrame:frame];
#endif
Using #ifdef DEBUG ensures that it doesn't go in the release build, but I would also recommend commenting it out when you're not using it since it has a performance impact. Credit goes to Robert Sanders and Prcela for the original code
Also if using ARC you may need to add "-fno-objc-arc" to prevent some build errors.
I have created an SDK kosher error reporter that includes:
The error message
The name of the file the error happens in
The line number the error happens on
The JavaScript callstack including parameters passed
It is part of the QuickConnectiPhone framework available from the sourceForge project
There is even an example application that shows how to send an error message to the Xcode terminal.
All you need to do is to surround your JavaScript code, including function definitions, etc. with try catch. It should look like this.
try{
//put your code here
}
catch(err){
logError(err);
}
It doesn't work really well with compilation errors but works with all others. Even anonymous functions.
The development blog is here
is here and includes links to the wiki, sourceForge, the google group, and twitter. Maybe this would help you out.
I have done this in firmware 1.x but not 2.x.
Here is the code I used in 1.x, it should at least help you on your way.
// Dismiss Javascript alerts and telephone confirms
/*- (void)alertSheet:(UIAlertSheet*)sheet buttonClicked:(int)button
{
if (button == 1)
{
[sheet setContext: nil];
}
[sheet dismiss];
}*/
// Javascript errors and logs
- (void) webView: (WebView*)webView addMessageToConsole: (NSDictionary*)dictionary
{
NSLog(#"Javascript log: %#", dictionary);
}
// Javascript alerts
- (void) webView: (WebView*)webView runJavaScriptAlertPanelWithMessage: (NSString*) message initiatedByFrame: (WebFrame*) frame
{
NSLog(#"Javascript Alert: %#", message);
UIAlertSheet *alertSheet = [[UIAlertSheet alloc] init];
[alertSheet setTitle: #"Javascript Alert"];
[alertSheet addButtonWithTitle: #"OK"];
[alertSheet setBodyText:message];
[alertSheet setDelegate: self];
[alertSheet setContext: self];
[alertSheet popupAlertAnimated:YES];
}
See exception handling in iOS7:
http://www.bignerdranch.com/blog/javascriptcore-example/
[context setExceptionHandler:^(JSContext *context, JSValue *value) {
NSLog(#"%#", value);
}];
First setup WebViewJavascriptBridge ,
then override console.error function.
In javascript
window.originConsoleError = console.error;
console.error = (msg) => {
window.originConsoleError(msg);
bridge.callHandler("sendConsoleLogToNative", {
action:action,
message:message
}, null)
};
In Objective-C
[self.bridge registerHandler:#"sendConsoleLogToNative" handler:^(id data, WVJBResponseCallback responseCallback) {
NSString *action = data[#"action"];
NSString *msg = data[#"message"];
if (isStringValid(action)){
if ([#"console.error" isEqualToString:action]){
NSLog(#"JS error :%#",msg);
}
}
}];
A simpler solution for some cases might be to just add Firebug Lite to the Web page.

Categories

Resources