How to read console logs of wkwebview programmatically
You can re-evaluate (override) Javascript console.log() default implementation to use window.webkit.messageHandlers.postMessage(msg) to pass message forwards instead. And then intercept the javascript postMessage(msg) call at native code using WKScriptMessageHandler ::didReceiveScriptMessage to get the logged message.
Step 1) Re-evaluate console.log default implementation to use postMessage()
// javascript to override console.log to use messageHandlers.postmessage
NSString * js = @"var console = { log: function(msg){window.webkit.messageHandlers.logging.postMessage(msg) } };";
// evaluate js to wkwebview
[self.webView evaluateJavaScript:js completionHandler:^(id _Nullable ignored, NSError * _Nullable error) {
if (error != nil)
NSLog(@"installation of console.log() failed: %@", error);
}];
Step 2) Intercept javascript postMessage in native code at WKScriptMessageHandler::didReceiveScriptMessage
- (void)viewDidLoad
{
// create message handler named "logging"
WKUserContentController *ucc = [[WKUserContentController alloc] init];
[ucc addScriptMessageHandler:self name:@"logging"];
// assign usercontentcontroller to configuration
WKWebViewConfiguration *configuration = [[WKWebViewConfiguration alloc] init];
[configuration setUserContentController:ucc];
// assign configuration to wkwebview
self.webView = [[WKWebView alloc] initWithFrame:CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height) configuration:configuration];
}
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message
{
// what ever were logged with console.log() in wkwebview arrives here in message.body property
NSLog(@"log: %@", message.body);
}
I needed a way to see JavaScript logs in Xcode's console. Based on the answer by noxo, here's what I came up with:
let overrideConsole = """
function log(emoji, type, args) {
window.webkit.messageHandlers.logging.postMessage(
`${emoji} JS ${type}: ${Object.values(args)
.map(v => typeof(v) === "undefined" ? "undefined" : typeof(v) === "object" ? JSON.stringify(v) : v.toString())
.map(v => v.substring(0, 3000)) // Limit msg to 3000 chars
.join(", ")}`
)
}
let originalLog = console.log
let originalWarn = console.warn
let originalError = console.error
let originalDebug = console.debug
console.log = function() { log("ð", "log", arguments); originalLog.apply(null, arguments) }
console.warn = function() { log("ð", "warning", arguments); originalWarn.apply(null, arguments) }
console.error = function() { log("ð", "error", arguments); originalError.apply(null, arguments) }
console.debug = function() { log("ð", "debug", arguments); originalDebug.apply(null, arguments) }
window.addEventListener("error", function(e) {
log("ð¥", "Uncaught", [`${e.message} at ${e.filename}:${e.lineno}:${e.colno}`])
})
"""
class LoggingMessageHandler: NSObject, WKScriptMessageHandler {
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
print(message.body)
}
}
let userContentController = WKUserContentController()
userContentController.add(LoggingMessageHandler(), name: "logging")
userContentController.addUserScript(WKUserScript(source: overrideConsole, injectionTime: .atDocumentStart, forMainFrameOnly: true))
let webViewConfig = WKWebViewConfiguration()
webViewConfig.userContentController = userContentController
let webView = WKWebView(frame: .zero, configuration: webViewConfig)
It has a few improvements:
- It still calls the original log function, in case you decide to look in the Web Inspector
- It reports from both
log
,warn
,error
anddebug
- It adds a nice emoji so you can easily distinguish the different kinds og logs and JS logs stands out in the Xcode console
- It logs all arguments given to
console.log
, not just the first one - It logs uncaught errors, in case you need that
This worked for me (Swift 4.2/5):
// inject JS to capture console.log output and send to iOS
let source = "function captureLog(msg) { window.webkit.messageHandlers.logHandler.postMessage(msg); } window.console.log = captureLog;"
let script = WKUserScript(source: source, injectionTime: .atDocumentEnd, forMainFrameOnly: false)
webView.configuration.userContentController.addUserScript(script)
// register the bridge script that listens for the output
webView.configuration.userContentController.add(self, name: "logHandler")
Then, conforming to the protocol WKScriptMessageHandler, pick up redirected console messages with the following:
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
if message.name == "logHandler" {
print("LOG: \(message.body)")
}
}
It's possible to connect Safari browser on you Mac to the WKWebView and get access to the console.
From Safari, open "Develop" tab and while the iOS Simulator is running with the WKWebView open - just click it to open the console. See: