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 and debug
  • 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:

enter image description here