When writing a iPhone / iPad app with a UIWebView, the console isn't visible.
this excellent answer shows how to trap errors, but I would like to use the console.log() as well.
After consulting with an esteemed colleague today he alerted me to the Safari Developer Toolkit, and how this can be connected to UIWebViews in the iOS Simulator for console output (and debugging!).
Steps:
Open Safari Preferences -> "Advanced" tab -> enable checkbox "Show Develop menu in menu bar"
Start app with UIWebView in iOS Simulator
Safari -> Develop -> i(Pad/Pod) Simulator -> [the name of your UIWebView file]
You can now drop complex (in my case, flot) Javascript and other stuff into UIWebViews and debug at will.
EDIT: As pointed out by #Joshua J McKinnon this strategy also works when debugging UIWebViews on a device. Simply enable Web Inspector on your device settings: Settings->Safari->Advanced->Web Inspector (cheers #Jeremy Wiebe)
UPDATE: WKWebView is supported too
I have a solution to log, using javascript, to the apps debug console.
It's a bit crude, but it works.
First, we define the console.log() function in javascript, which opens and immediately removes an iframe with a ios-log: url.
// Debug
console = new Object();
console.log = function(log) {
var iframe = document.createElement("IFRAME");
iframe.setAttribute("src", "ios-log:#iOS#" + log);
document.documentElement.appendChild(iframe);
iframe.parentNode.removeChild(iframe);
iframe = null;
};
console.debug = console.log;
console.info = console.log;
console.warn = console.log;
console.error = console.log;
Now we have to catch this URL in the UIWebViewDelegate in the iOS app using the shouldStartLoadWithRequest function.
- (BOOL)webView:(UIWebView *)webView2
shouldStartLoadWithRequest:(NSURLRequest *)request
navigationType:(UIWebViewNavigationType)navigationType {
NSString *requestString = [[[request URL] absoluteString] stringByReplacingPercentEscapesUsingEncoding: NSUTF8StringEncoding];
//NSLog(requestString);
if ([requestString hasPrefix:#"ios-log:"]) {
NSString* logString = [[requestString componentsSeparatedByString:#":#iOS#"] objectAtIndex:1];
NSLog(#"UIWebView console: %#", logString);
return NO;
}
return YES;
}
Here's the Swift solution:
(It's a bit of a hack to get the context)
You create the UIWebView.
Get the internal context and override the console.log() javascript function.
self.webView = UIWebView()
self.webView.delegate = self
let context = self.webView.valueForKeyPath("documentView.webView.mainFrame.javaScriptContext") as! JSContext
let logFunction : #convention(block) (String) -> Void =
{
(msg: String) in
NSLog("Console: %#", msg)
}
context.objectForKeyedSubscript("console").setObject(unsafeBitCast(logFunction, AnyObject.self),
forKeyedSubscript: "log")
Starting from iOS7, you can use native Javascript bridge. Something as simple as following
#import <JavaScriptCore/JavaScriptCore.h>
JSContext *ctx = [webview valueForKeyPath:#"documentView.webView.mainFrame.javaScriptContext"];
ctx[#"console"][#"log"] = ^(JSValue * msg) {
NSLog(#"JavaScript %# log message: %#", [JSContext currentContext], msg);
};
NativeBridge is very helpful for communicating from a UIWebView to Objective-C. You can use it to pass console logs and call Objective-C functions.
https://github.com/ochameau/NativeBridge
console = new Object();
console.log = function(log) {
NativeBridge.call("logToConsole", [log]);
};
console.debug = console.log;
console.info = console.log;
console.warn = console.log;
console.error = console.log;
window.onerror = function(error, url, line) {
console.log('ERROR: '+error+' URL:'+url+' L:'+line);
};
The advantage of this technique is that things like newlines in log messages are preserved.
Swift 5
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
webView.evaluateJavaScript("your javascript string") { (value, error) in
if let errorMessage = (error! as NSError).userInfo["WKJavaScriptExceptionMessage"] as? String {
print(errorMessage)
}
}
}
Tried Leslie Godwin's fix but was getting this error:
'objectForKeyedSubscript' is unavailable: use subscripting
For Swift 2.2, here's what worked for me:
You will need to import JavaScriptCore for this code to compile:
import JavaScriptCore
if let context = webView.valueForKeyPath("documentView.webView.mainFrame.javaScriptContext") {
context.evaluateScript("var console = { log: function(message) { _consoleLog(message) } }")
let consoleLog: #convention(block) String -> Void = { message in
print("javascript_log: " + message)
}
context.setObject(unsafeBitCast(consoleLog, AnyObject.self), forKeyedSubscript: "_consoleLog")
}
Then in your javascript code, calling console.log("_your_log_") will print in Xcode console.
Better yet, add this code as an extension to UIWebView:
import JavaScriptCore
extension UIWebView {
public func hijackConsoleLog() {
if let context = valueForKeyPath("documentView.webView.mainFrame.javaScriptContext") {
context.evaluateScript("var console = { log: function(message) { _consoleLog(message) } }")
let consoleLog: #convention(block) String -> Void = { message in
print("javascript_log: " + message)
}
context.setObject(unsafeBitCast(consoleLog, AnyObject.self), forKeyedSubscript: "_consoleLog")
}
}
}
And then call this method during your UIWebView initialization step:
let webView = UIWebView(frame: CGRectZero)
webView.hijackConsoleLog()
Related
I need my swift class to interact with the html and javascript in a wkwebview that it references, in particular, feed it a variable.
I thought I would start simply by trying to get the webview to retrieve something using getElementByID following the code here. However, it is not working.
Here is code:
HTML file
<div id="username">#twostraws</div>
Swift File
let webView = WKWebView()
override func viewDidLoad() {
super.viewDidLoad()
if let url = Bundle.main.url(forResource: "myHTML", withExtension: "html") {
webView.loadFileURL(url, allowingReadAccessTo: url.deletingLastPathComponent())
}
webView.evaluateJavaScript("document.getElementById('username').innerText") { (result, error) in
if let result = result {
print("what is in div",result)
}
else {
print("didn't find it")
}
}
override func loadView() {
self.view = webView
}
The webview is loading, however nothing is getting retrieved by evaluateJavascript.
Edit:
This is error message:
A JavaScript exception occurred" UserInfo={WKJavaScriptExceptionLineNumber=1, WKJavaScriptExceptionMessage=TypeError: null is not an object (evaluating 'document.getElementById('username').innerText'), WKJavaScriptExceptionColumnNumber=32, WKJavaScriptExceptionSourceURL=about:blank, NSLocalizedDescription=A JavaScript exception occurred})
Thanks for any suggestions.
I'm trying to get WKWebView to load this calendar page (csbcsaints.org/calendar) and immediately go to the next month by clicking that arrow button. Following along with WWDC 2014's 'Introducing the Modern WebKit API,' here's what I have so far:
class WebViewController: UIViewController {
let jsString = "document.getElementById('evcal_next').click()"
override func viewDidLoad() {
super.viewDidLoad()
let script = WKUserScript(source: jsString, injectionTime: .atDocumentEnd, forMainFrameOnly: true)
let contentController = WKUserContentController()
contentController.addUserScript(script)
let myConfiguration = WKWebViewConfiguration()
myConfiguration.userContentController = contentController
myConfiguration.preferences.javaScriptEnabled = true
let webView = WKWebView(frame: .zero, configuration: myConfiguration)
self.view = webView
if let url = URL(string: "https://csbcsaints.org/calendar") {
let request = URLRequest(url: url)
webView.load(request)
}
}
}
When that didn't work with either .atDocumentStart or .atDocumentEnd, I tried calling webView.evaluateJavaScript(jsString) after loading the page instead, but it only returned an error:
Error Domain=WKErrorDomain Code=4
"A JavaScript exception occurred"
UserInfo={
WKJavaScriptExceptionLineNumber=1,
WKJavaScriptExceptionMessage=
TypeError: null is not an object (evaluating 'document.getElementById('evcal_next').click'),
WKJavaScriptExceptionColumnNumber=38,
WKJavaScriptExceptionSourceURL=about:blank,
NSLocalizedDescription=A JavaScript exception occurred
}
It seems to say that the command I passed was invalid, but I'm not sure, since running jsString in Safari's console performed the effect I wanted. I'm relatively experienced with Swift and iOS, but new to JavaScript, so any help would be appreciated.
You should assign the delegate of the webView like:
webView.delegate = self
and do it inside didEnd... method:
extension WebViewController: WKNavigationDelegate {
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
/* This is where you can perform DOM with JS in webview */
}
}
I've got a WBWebview running into a NSPopover.
I've followed this guide in order to be able to send data back to my Swift application from JS.
Unfortunately, userContentController is never called in the Swift app.
Here's my Swift app View controller code:
class GSViewController: NSViewController, WKScriptMessageHandler, WKNavigationDelegate {
var webView: WKWebView?
var webConfig:WKWebViewConfiguration {
get {
// Create WKWebViewConfiguration instance
let webCfg:WKWebViewConfiguration = WKWebViewConfiguration()
// Setup WKUserContentController instance for injecting user script
let userController:WKUserContentController = WKUserContentController()
// Add a script message handler for receiving "buttonClicked" event notifications posted from the JS document using window.webkit.messageHandlers.buttonClicked.postMessage script message
userController.addScriptMessageHandler(self, name: "buttonClicked")
// Get script that's to be injected into the document
let js:String = buttonClickEventTriggeredScriptToAddToDocument()
// Specify when and where and what user script needs to be injected into the web document
let userScript:WKUserScript = WKUserScript(source: js, injectionTime: WKUserScriptInjectionTime.AtDocumentEnd, forMainFrameOnly: false)
// Add the user script to the WKUserContentController instance
userController.addUserScript(userScript)
// Configure the WKWebViewConfiguration instance with the WKUserContentController
webCfg.userContentController = userController;
return webCfg;
}
}
override func loadView() {
super.loadView()
}
override func viewDidLoad() {
super.viewDidLoad()
self.webView = WKWebView(frame: self.view.frame, configuration: webConfig)
self.webView!.navigationDelegate = self
self.view = webView!
let username = NSUserName()
let url = NSURL(string:"someUrl")
let req = NSURLRequest(URL:url!)
self.webView!.loadRequest(req)
self.view.frame = CGRectMake(0, 0, 440, 600)
}
func buttonClickEventTriggeredScriptToAddToDocument() ->String{
let script:String = "webkit.messageHandlers.callbackHandler.postMessage('Does it works?');"
return script;
}
// WKNavigationDelegate
func webView(webView: WKWebView, didFinishNavigation navigation: WKNavigation!) {
NSLog("%s", #function)
}
func webView(webView: WKWebView, didFailNavigation navigation: WKNavigation!, withError error: NSError) {
NSLog("%s. With Error %#", #function, error)
showAlertWithMessage("Failed to load file with error \(error.localizedDescription)!")
}
func userContentController(userContentController: WKUserContentController, didReceiveScriptMessage message: WKScriptMessage) {
print("called")
if(message.name == "callbackHandler") {
print("It does ! \(message.body)")
}
}
// Helper
func showAlertWithMessage(message:String) {
let myPopup:NSAlert = NSAlert()
myPopup.messageText = message
myPopup.alertStyle = NSAlertStyle.WarningAlertStyle
myPopup.addButtonWithTitle("OK")
myPopup.runModal()
}
}
I'm trying to inject directly the call to Swift callback webkit.messageHandlers.callbackHandler.postMessage('Does it works?');, but it seems not to work.
With different test, I've discovered that the injection actually works in the Swift -> JS way (I've been able to modify CSS of visible HTML elements via injection of JQuery code), but the JS -> Swift bridge just doesn't seem to make it.
Does anyone have an idea why?
I'm under OSX 10.11.6, running XCode 7.3.6.
I'm really new into Swift and OSX programming in general, so doesn't hesitate to point out any element I'm missing. I'm voluntary omitting the JS my page uses, since I'm not using any of it in the example.
Thanks.
Found it, was totally my bad.
I forgot to rename the message handler when I added my script to the webview configuration.
userController.addScriptMessageHandler(self, name: "callbackHandler") // was originally "ButtonClicked"
How can i monitor requests on WKWebview?
I'v tried using NSURLprotocol (canInitWithRequest) but it won't monitor ajax requests (XHR), only navigation requests(document requests)
Finally I solved it
Since I don't have control over the web view content, I injected to the WKWebview a java script that include a jQuery AJAX request listener.
When the listener catches a request it sends the native app the request body in the method:
webkit.messageHandlers.callbackHandler.postMessage(data);
The native app catches the message in a delegate called:
(void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message
and perform the corresponding actions
here is the relevant code:
ajaxHandler.js -
//Every time an Ajax call is being invoked the listener will recognize it and will call the native app with the request details
$( document ).ajaxSend(function( event, request, settings ) {
callNativeApp (settings.data);
});
function callNativeApp (data) {
try {
webkit.messageHandlers.callbackHandler.postMessage(data);
}
catch(err) {
console.log('The native context does not exist yet');
}
}
My ViewController delegate are:
#interface BrowserViewController : UIViewController <UIWebViewDelegate, WKUIDelegate, WKNavigationDelegate, WKScriptMessageHandler, UIWebViewDelegate>
And in my viewDidLoad(), I'm creating a WKWebView:
WKWebViewConfiguration *configuration = [[WKWebViewConfiguration alloc]init];
[self addUserScriptToUserContentController:configuration.userContentController];
appWebView = [[WKWebView alloc]initWithFrame:self.view.frame configuration:configuration];
appWebView.UIDelegate = self;
appWebView.navigationDelegate = self;
[appWebView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString: #"http://#############"]]];
Here is the addUserScriptToUserContentController:
- (void) addUserScriptToUserContentController:(WKUserContentController *) userContentController{
NSString *jsHandler = [NSString stringWithContentsOfURL:[[NSBundle mainBundle]URLForResource:#"ajaxHandler" withExtension:#"js"] encoding:NSUTF8StringEncoding error:NULL];
WKUserScript *ajaxHandler = [[WKUserScript alloc]initWithSource:jsHandler injectionTime:WKUserScriptInjectionTimeAtDocumentEnd forMainFrameOnly:NO];
[userContentController addScriptMessageHandler:self name:#"callbackHandler"];
[userContentController addUserScript:ajaxHandler];
}
#Benzi Heler answer is great, but it uses jQuery which seems like is not working in WKWebView anymore, so I have found solution without using jQuery.
Here is ViewController implementation that lets you be notified every AJAX request is completed in WKWebView:
import UIKit
import WebKit
class WebViewController: UIViewController {
private var wkWebView: WKWebView!
private let handler = "handler"
override func viewDidLoad() {
super.viewDidLoad()
let config = WKWebViewConfiguration()
let userScript = WKUserScript(source: getScript(), injectionTime: .atDocumentStart, forMainFrameOnly: false)
config.userContentController.addUserScript(userScript)
config.userContentController.add(self, name: handler)
wkWebView = WKWebView(frame: view.bounds, configuration: config)
view.addSubview(wkWebView)
if let url = URL(string: "YOUR AJAX WEBSITE") {
wkWebView.load(URLRequest(url: url))
} else {
print("Wrong URL!")
}
}
private func getScript() -> String {
if let filepath = Bundle.main.path(forResource: "script", ofType: "js") {
do {
return try String(contentsOfFile: filepath)
} catch {
print(error)
}
} else {
print("script.js not found!")
}
return ""
}
}
extension WebViewController: WKScriptMessageHandler {
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
if let dict = message.body as? Dictionary<String, AnyObject>, let status = dict["status"] as? Int, let responseUrl = dict["responseURL"] as? String {
print(status)
print(responseUrl)
}
}
}
Pretty standard implementation. There is a WKWebView created programmatically. There is injected script that is loaded from script.js file.
And the most important part is script.js file:
var open = XMLHttpRequest.prototype.open;
XMLHttpRequest.prototype.open = function() {
this.addEventListener("load", function() {
var message = {"status" : this.status, "responseURL" : this.responseURL}
webkit.messageHandlers.handler.postMessage(message);
});
open.apply(this, arguments);
};
userContentController delegate method will be called every time there is AJAX request loaded. I'm passing there status and responseURL, because this was what I needed in my case, but you can also get more informations about request. Here is the list of all properties and methods available:
https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest
My solution is inspired by this answer written by #John Culviner:
https://stackoverflow.com/a/27363569/3448282
If you have control of the content inside the WkWebView you can send messages to your native app using window.webkit.messageHandlers whenever you make an ajax request, which will be received as a WKScriptMessage that can be processed by whatever you've designated as your WKScriptMessageHandler. The messages can contain whatever information you wish, and will be automatically converted into native objects/values in your Objective-C or Swift code.
If you don't have control over the content you can still do this by injecting your own JavaScript via a WKUserScript to track ajax requests and send back messages using the method stated above.
You can use this to respond to requests from the WKWebView. It works similar to UIWebView.
- (void)webView:(WKWebView *)webView2 decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler {
if (navigationAction.navigationType == WKNavigationTypeLinkActivated) {
NSString *url = [navigationAction.request.URL absoluteString];
// Handle URL request internally
}
decisionHandler(WKNavigationActionPolicyAllow); // Will continue processing request
decisionHandler(WKNavigationActionPolicyCancel); // Cancels request
}
I've put together a very simple program that uses JavaScriptCore to evaluate JS:
#import <CoreFoundation/CoreFoundation.h>
#import <JavaScriptCore/JavaScriptCore.h>
int main(int argc, const char * argv[])
{
JSGlobalContextRef ctx = JSGlobalContextCreate(NULL);
FILE *f = fopen(argv[1],"r");
char * buffer = malloc(10000000);
fread(buffer,1,10000000,f);
CFStringRef strs = CFStringCreateWithCString(NULL, buffer, kCFStringEncodingASCII);
JSStringRef jsstr = JSStringCreateWithCFString(strs);
JSValueRef result = JSEvaluateScript(ctx, jsstr, NULL, NULL, 0, NULL);
double res = JSValueToNumber(ctx, result, NULL);
JSGlobalContextRelease(ctx);
printf("%lf\n", res);
return 0;
}
The idea here is that the last value is expected to be a Number, and that value is printed. This works for valid javascript code, such as
var square = function(x) { return x*x; }; square(4)
However, if the code tries to perform a console.log, the program segfaults. Is there a log function available in JSC or do I have to roll my own?
You do have to provide your own console log if using the JavaScriptCore framework from Mac or IOS.
Here is some code that worked for me (sorry it is Objective-C rather than standard C as per your code above):
JSContext *javascriptContext = [[JSContext alloc] init];
javascriptContext[#"consoleLog"] = ^(NSString *message) {
NSLog(#"Javascript log: %#",message);
};
Then you use it from Javascript by:
consoleLog("My debug message");
Note that I have tried to define a vararg version (log taking multiple parameters) but I couldn't get this to work correctly across the framework api.
Note that this solution uses features introduced with the new Objective-C API for the JavaScriptCore.framework introduced at the same time as IOS 7. If you are looking for an intro to this well-integrated bridge between Objective-C and Javascript, check out the 2013 WWDC introduction "Integrating JavaScript into Native Apps" session on Apple's developer network: https://developer.apple.com/videos/wwdc/2013/?id=615
Update to answer:
For those of you wanting to maximise your javascript code reuse without refactoring, I've managed to get a version working that declares a log of the form console.log() :
JSContext *javascriptContext = [[JSContext alloc] init];
[javascriptContext evaluateScript:#"var console = {}"];
javascriptContext[#"console"][#"log"] = ^(NSString *message) {
NSLog(#"Javascript log: %#",message);
};
Then you use it from Javascript by:
console.log("My debug message");
Swift 3.0
let javascriptContext = JSContext()
javascriptContext?.evaluateScript("var console = { log: function(message) { _consoleLog(message) } }")
let consoleLog: #convention(block) (String) -> Void = { message in
print("console.log: " + message)
}
javascriptContext?.setObject(unsafeBitCast(consoleLog, to: AnyObject.self), forKeyedSubscript: "_consoleLog" as (NSCopying & NSObjectProtocol)!)
Swift 2.1
let javascriptContext = JSContext()
javascriptContext.evaluateScript("var console = { log: function(message) { _consoleLog(message) } }")
let consoleLog: #convention(block) String -> Void = { message in
print("console.log: " + message)
}
javascriptContext.setObject(unsafeBitCast(consoleLog, AnyObject.self), forKeyedSubscript: "_consoleLog")
Then you use it from Javascript by:
console.log("My debug message");
self.jsContext = JSContext()
self.jsContext.evaluateScript(...)
let logFunction: #convention(block) (String) -> Void = { (string: String) in
print(string)
}
self.jsContext.setObject(logFunction, forKeyedSubscript: "consoleLog" as NSCopying & NSObjectProtocol)
You can debug JS file attached to context in Safari.
Steps:
1) Start Safari
2) In Safari, enable the Develop menu by going to "Preferences" -> "Advanced" -> "Show Develop menu in menu bar"
3) Go to Develop menu -> "Simulator" or name of your computer -> select "Automatically show web inspector for JSContexts" and "Automatically pause connecting to JSContexts"
4) Re-run your project and Safari should auto-show the web inspector
Swift 5.0
The other suggestions didn't work for me, so I found a web post that explains how to do it now.Essentially
let logFunction: #convention(block) (String) -> Void = { string in
print("JS_Console:", string)
}
if let console = context.objectForKeyedSubscript("console") {
console.setObject(logFunction, forKeyedSubscript: "log") // works for me
// is this needed? "console.setObject(unsafeBitCast(logFunction, to: AnyObject.self), forKeyedSubscript: "log")
}
log.console is variadic, but I could find no way to utilize it even though the link above suggests it's possible. What I did discover though is that you can use JavaScript interpolation to get values, for example:
console.log(`getCombinedFrameYaw: ${frameYaw} rot=${pathRotation}`)