48

I am trying to read the console logs of a webapp that is loaded in my WKWebView programmatically.

So far in my research it's not possible.

How can I achieve this?

Pang
  • 9,564
  • 146
  • 81
  • 122
NaveenKumar
  • 600
  • 1
  • 5
  • 18
  • why you need to read these logs while these messages are available in your js files? you can simple store anywhere. – Jitendra Tiwari May 11 '16 at 10:19
  • 1
    This webapps that's going to be loaded in Webview is already developed and we can't expect change from webdevelopers for this..thats Y – NaveenKumar May 11 '16 at 10:37
  • swift should learn this from android, really. it's sucks when you have to do do so much to just get the console working. using the callback to do this for the moment, but really there should be a better way – MartianMartian May 28 '22 at 14:28

9 Answers9

44

It's possible to connect Safari browser on your 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

Pang
  • 9,564
  • 146
  • 81
  • 122
Oded Regev
  • 4,065
  • 2
  • 38
  • 50
  • 2
    I see empty console in wkwebview, but if I open that link in safari I see logs. :( – えるまる Mar 21 '19 at 10:00
  • 5
    I do not have a "Simulator" option in my Safari's Develop menu, even when the simulator is running. I have the latest Xcode and the latest Safari. This has been the case for months. – Curtis Apr 30 '19 at 04:13
  • 1
    Tap on Safari at the top menu, from there: Safari --> Preferences --> Advanced --> "Show Develop menu" – Oded Regev Apr 30 '19 at 12:46
  • 6
    this is not programatically, not answering the question – Amos Aug 08 '19 at 08:38
  • Now I have My iPhone as an option to inspect, but when I do it's blank. No logs even though it is calling console.log and it won't let me enter commands either. – Curtis May 18 '20 at 01:17
  • I have "Show develop menu" but develop menu does not have "Simulator" option. Xcode 11.5 safari 13.1.2 – Anton Tropashko Aug 28 '20 at 12:41
  • 1
    Hope you have a good day, You just helped me kill 7 days old hard skinned bug – Mehul Prajapati Apr 14 '22 at 09:02
  • Reminder to enable Web Inspector on your device if you're trying to inspect device console logs. (I had forgotten) – David Mohundro Jul 08 '22 at 19:09
34

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)")  
    }
}
Richard West-Soley
  • 1,514
  • 2
  • 11
  • 6
  • 2
    This one worked for me (iOS 13.5, Objective-C, Xcode 11.5) – Pierre Jul 01 '20 at 10:10
  • 1
    remember to ```import WebKit``` – spodell Oct 13 '20 at 16:27
  • 1
    Great thanks. You might want to also capture `console.warn` and `console.error` also, ie. add to end of `let source ...` line. – xaphod Oct 20 '20 at 15:31
  • ... I ended up posting my own answer because I needed `console.log`'s string substitution to work. – xaphod Oct 20 '20 at 16:20
  • to capture error mesaj use this let source = "function captureLog(msg) { window.webkit.messageHandlers.logHandler.postMessage(msg); } window.onerror = captureLog;" – Ucdemir Jan 06 '21 at 14:39
  • Overriding console.log is a really bad idea, since it'll cause devtools to display your console.log for every message instead of the real line number. – Glenn Maynard Jan 16 '23 at 02:53
  • 1
    You might want to use `injectionTime: .atDocumentStart` instead, to not miss some of early logs – OlegWock May 31 '23 at 17:34
25

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
Soeholm
  • 1,102
  • 14
  • 15
  • 2020-08-28 16:09:24.170424+0300 UP[6113:11057331] *** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Actions added to UIAlertController must have a title' *** First throw call stack: – Anton Tropashko Aug 28 '20 at 13:10
  • was a fluke or unrelated to your logger. works now. thank you. – Anton Tropashko Aug 28 '20 at 13:45
10

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);
}
mah
  • 39,056
  • 9
  • 76
  • 93
Erkki Nokso-Koivisto
  • 1,203
  • 15
  • 19
  • 2
    Has anyone else gotten this solution to work (or the other one below?). I tried both and ...didReceiveScriptMessage: is never called. – Locksleyu Feb 27 '19 at 23:43
  • @Locksleyu: I tried by adding it as user script at the end of the page. It is not called. If I use this code `function logging(msg){window.webkit.messageHandlers.logging.postMessage(msg) };` and call it with `logging('testing');` the `didReceiveScriptMessage` method is called. Don't know if there is a difference with `evaluateJavaScript` as proposed in this solution. For me that means, that you can't use `console.log()`. Instead you have to use your custom JS function, which has some identifier in `message` so that you can differentiate between `log` and other methods. Or use multiple handler. – testing Mar 01 '19 at 12:53
  • This works but there are minor problems in the post preventing it from working; I'll edit momentarily. The most important change is to add a single } at the end of the JavaScript that gets evaluated in the first step. – mah Aug 15 '19 at 14:00
  • watch out if you have catch(e){console.log(e)} it won't even send a message. Doesn't work with logging objects but you can do e.toString() – Curtis May 18 '20 at 03:45
5

Here's a tweak of Richard's answer (well, several answers use the same method) that handles console.log's string-substitution.

I needed this because React Errors are logged with strings like Hey you had an error:%s, and of course I needed to see what %s was. I also used .documentStart since I want to catch errors right away.

Adding the WKUserScript to the WKUserContentController (which is part of the WKWebViewConfiguration you init the WKWebView with):

        let source = """
        function sprintf(str, ...args) { return args.reduce((_str, val) => _str.replace(/%s|%v|%d|%f|%d/, val), str); }
        function captureLog(str, ...args) { var msg = sprintf(str, ...args); window.webkit.messageHandlers.logHandler.postMessage({ msg, level: 'log' }); }
        function captureWarn(str, ...args) { var msg = sprintf(str, ...args); window.webkit.messageHandlers.logHandler.postMessage({ msg, level: 'warn' }); }
        function captureError(str, ...args) { var msg = sprintf(str, ...args); window.webkit.messageHandlers.logHandler.postMessage({ msg, level: 'error' }); }
        window.console.error = captureError; window.console.warn = captureWarn;
        window.console.log = captureLog; window.console.debug = captureLog; window.console.info = captureLog;
        """
        let script = WKUserScript(source: source, injectionTime: .atDocumentStart, forMainFrameOnly: false)

        let config = WKWebViewConfiguration.init()
        let userContentController = WKUserContentController()
        config.userContentController = userContentController
        userContentController.addUserScript(script)

        // ... potentially somewhere else ...
        let webView = WKWebView(frame: CGRect.zero, configuration: config)

... and handling it in whatever class (hint: must be an NSObject) that you implement WKScriptMessageHandler in:

    func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
        guard
            let body = message.body as? [String:Any],
            let msg = body["msg"] as? String,
            let level = body["level"] as? String
        else {
            assert(false)
            return
        }
        NSLog("WEB \(level): \(msg)")
}

Replace NSLog with whatever you actually use (ie. your custom Swift log handler function).

xaphod
  • 6,392
  • 2
  • 37
  • 45
5

Some of the otherwise great solutions on this page may mysteriously crash your page. I have found this is because some objects (e.g. events) cause JSON.stringify to throw an exeception (for example see How to stringify event object?)

For simplicity I catch the exception and move on. While I am at it, I encapsulate the logic into a class so that the usage with your userContentController object becomes a one-liner:

        WebLogHandler().register(with: userContentController)

The source for the implementation follows. My injected script is simpler than some here, for my purposes I didn't actually want to hear from warn / error etc so I could focus on log, and I didn't want to see any emoji, but otherwise this is similar to Soeholm's answer. It's an exercise to make the class more configurable with those options or to finesse the handling of the problematic objects so as to stringify them at least in part.

class WebLogHandler: NSObject, WKScriptMessageHandler {

let messageName = "logHandler"

lazy var scriptSource:String = {
    return """

    function stringifySafe(obj) {
        try {
            return JSON.stringify(obj)
        }
        catch (err) {
            return "Stringify error"
        }
    }

    function log(type, args) {
      window.webkit.messageHandlers.\(messageName).postMessage(
        `JS ${type}: ${Object.values(args)
          .map(v => typeof(v) === "undefined" ? "undefined" : typeof(v) === "object" ? stringifySafe(v) : v.toString())
          .map(v => v.substring(0, 3000)) // Limit msg to 3000 chars
          .join(", ")}`
      )
    }

    let originalLog = console.log

    console.log = function() { log("log", arguments); originalLog.apply(null, arguments) }
    """
    
}()


func register(with userContentController: WKUserContentController) {
    userContentController.add(self, name: messageName)
    
    // inject JS to capture console.log output and send to iOS
    let script = WKUserScript(source: scriptSource,
                              injectionTime: .atDocumentStart,
                              forMainFrameOnly: false)
    userContentController.addUserScript(script)
}

func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
    
    print(message.body)
}

}

biomiker
  • 3,108
  • 1
  • 29
  • 26
3

Normally the console logging is defined in js as

    "window.addEventListener("message",function(e){console.log(e.data)});"

My answer was adapted from handling-javascript-events-in-wkwebview!

Initialize WKWebView with configuration

    let config = WKWebViewConfiguration()
    let source = "document.addEventListener('message', function(e){
     window.webkit.messageHandlers.iosListener.postMessage(e.data); })"
    let script = WKUserScript(source: source, injectionTime: .atDocumentEnd, forMainFrameOnly: false)
    config.userContentController.addUserScript(script)
    config.userContentController.add(self, name: "iosListener")
    webView = WKWebView(frame: UIScreen.main.bounds, configuration: config)

Or use KVO observe property "estimatedProgress" and inject js by evalueate JavaScript

    -(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
    {
        if ([keyPath isEqualToString:@"estimatedProgress"])
        {
            CGFloat progress = [change[NSKeyValueChangeNewKey] floatValue];
            if (progress>= 0.9)
            {
                NSString *jsCmd = @"window.addEventListener(\"message\",function(e){window.webkit.messageHandlers.iosListener.postMessage(e.data)});";
        //@"document.addEventListener('click', function(e){ window.webkit.messageHandlers.iosListener.postMessage('Customize click'); })";

                [_webView evaluateJavaScript:jsCmd completionHandler:^(id _Nullable obj, NSError * _Nullable error) {
                    NSLog(@"error:%@",error);
                }];
            }
        }
    }

implement the WKScriptMessageHandler protocol to receive the message:

    func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) 
    {
            print("message: \(message.body)")
            // and whatever other actions you want to take
    }
  • I've used the same code but it dint work for some reason – Chetan Rajagiri Apr 22 '20 at 13:01
  • KVO is brittle and it's easy to end up with crashes if you make a mistake. I'd suggest using the `WKScriptMessageHandler`-based approaches in other answers instead. – xaphod Oct 20 '20 at 16:21
1

Swift 4.2 & 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)
          }
      }
 }
Barath
  • 1,656
  • 19
  • 19
-7

Please use this beautiful in-app "Bridge"

Edit:

self.webView = [[WBWKWebView alloc] initWithFrame:self.view.bounds];
self.webView.JSBridge.interfaceName = @"WKWebViewBridge";
WBWebDebugConsoleViewController * controller = [[WBWebDebugConsoleViewController alloc] initWithConsole:_webView.console];

Then , you can use the delegate method:

- (void)webDebugInspectCurrentSelectedElement:(id)sender
{
// To use the referenced log values
}
itechnician
  • 1,645
  • 1
  • 14
  • 24