By using the WKWebView apps can load and display either remote webpages or local HTML stored in the app bundle. In both cases the web content is often developed by the same team and there is a need to be able to debug it while running in the WKWebView. Unfortunately there is no console like browsers provide for developers. The console output is missing.

Based on this example this blog post provides a guide on how to add polyfills to WKWebView with native code interaction. As a result you will be able to catch the console output of a webpage loaded in a WKWebView and print it to the Xcode debug console.

First things first

For simplicity ourWKWebView in this example will load a webpage from the app bundle. You could also host it on your server and let the WKWebView load it from there.

Create a new project in Xcode using the Single View App template and add the following simple test page as consoleTest.html file to the bundle.

<!DOCTYPE html>
<html>
<body>
    <h1>Console Test</h1>
    <button type="button" onclick="console.log('Hello world!')">Log message</button>
    <button type="button" onclick="console.error('Uhhh error!')">Log error</button>
</body>
</html>

Setup the WKWebView

Open the ViewController.swift file. In viewDidLoad add a WKWebView to the view and load the html resource which we created above.

let configuration = WKWebViewConfiguration()

let webView = WKWebView(frame: CGRect(), configuration: configuration)
view.addSubview(webView)
webView.translatesAutoresizingMaskIntoConstraints = false
webView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
webView.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true
webView.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
webView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true

if let url = Bundle.main.url(forResource: "consoleTest", withExtension: "html") {
    webView.loadFileURL(url, allowingReadAccessTo: url.deletingLastPathComponent())
}

Build the bridge

We will now setup a bridge between the WKWebView and the ViewController for handling the console functions.

Create a new file and name it domConsole.js. Add the following code to the file. It provides polyfills for the log and the error functions.

const console = {
    log(message) {
        webkit.messageHandlers.consoleLog.postMessage(message);
    },
    error(message) {
        webkit.messageHandlers.consoleError.postMessage(message);
    }
};

To inject these polyfills into the webpage we make use of the WKUserContentController. We also need to setup the ViewController as handler for the two polyfills.

let contentController = WKUserContentController();
let filePath = Bundle.main.path(forResource: "domConsole", ofType: "js")!
let scriptSource = try! String(contentsOfFile: filePath)
let userScript = WKUserScript(source: scriptSource, injectionTime: WKUserScriptInjectionTime.atDocumentStart, forMainFrameOnly: false)
contentController.addUserScript(userScript)

contentController.add(self, name: "consoleLog")
contentController.add(self, name: "consoleError")

A compiler error will now show up because the ViewController does not conform to the WKScriptMessageHandler protocol. This happens because we added the ViewController as a script message handler to the WKUserContentController. We will fix this by adding an extension for the ViewController and provide native handling for the polyfills.

extension ViewController: WKScriptMessageHandler {

    func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
        DispatchQueue.main.async {
            switch message.name {
            case "consoleLog":
                guard let logMessage = message.body as? String else { return }
                print("console.log:", logMessage)
            case "consoleError":
                guard let logMessage = message.body as? String else { return }
                print("console.error:", logMessage)
            default:
                break
            }
        }
    }
}

Make it work

When running the app in the simulator you will notice that it still doesn’t work. There is one piece missing.
At the place where we setup the WKWebViewConfiguration we need to associate the user content controller with the web view.

let configuration = WKWebViewConfiguration()
configuration.userContentController = contentController

let webView = WKWebView(frame: CGRect(), configuration: configuration)

Start the simulator again and use the buttons on the webpage to produce console messages. Observe the Xcode debug console. Ta-da! The console messages from the webpage show up there.

Being able to get the console output helped me a lot to debug webpages while having them running in a WKWebView.