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
.