iOS 14 introduced a new way to receive javascript calls and provide a response using WKScriptMessageHandlerWithReply instead of WKScriptMessageHandler (inside a WebKit view). However the documentation is basically nonexistent. How does this work?
Asked
Active
Viewed 3,383 times
1 Answers
14
I dug into this a bit and found it uses Javascript Promises to provide a callback mechanism (and the response from the app code back to the javascript must be async).
Here's some sample code to illustrate:
The swift code:
import UIKit
import WebKit
import PureLayout
final class ViewController: UIViewController {
var webView : WKWebView?
let JavaScriptAPIObjectName = "namespaceWithinTheInjectedJSCode"
override func viewDidLoad() {
super.viewDidLoad()
//-------
guard let scriptPath = Bundle.main.path(forResource: "script", ofType: "js"),
let scriptSource = try? String(contentsOfFile: scriptPath) else { return }
let userScript = WKUserScript(source: scriptSource, injectionTime: .atDocumentEnd, forMainFrameOnly: true)
let config = WKWebViewConfiguration()
let userContentController = WKUserContentController()
userContentController.addUserScript(userScript)
// REQUIRES IOS14
if #available(iOS 14, *){
userContentController.addScriptMessageHandler(self, contentWorld: .page, name: JavaScriptAPIObjectName)
}
config.userContentController = userContentController
webView = WKWebView(frame: .zero, configuration: config)
if let webView = webView{
view.addSubview(webView)
webView.autoPinEdgesToSuperviewMargins() // using PureLayout for easy AutoLayout syntax
if let htmlPath = Bundle.main.url(forResource: "page", withExtension: "html"){
webView.loadFileURL( htmlPath, allowingReadAccessTo: htmlPath);
}
}
}
// need to deinit and remove webview stuff
deinit {
if let webView = webView{
let ucc = webView.configuration.userContentController
ucc.removeAllUserScripts()
ucc.removeScriptMessageHandler(forName:JavaScriptAPIObjectName)
}
}
}
extension ViewController: WKScriptMessageHandlerWithReply {
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage, replyHandler: @escaping (Any?, String?) -> Void) {
if message.name == JavaScriptAPIObjectName, let messageBody = message.body as? String {
print(messageBody)
replyHandler( 2.2, nil ) // first var is success return val, second is err string if error
}
}
}
This is the script.js loaded via that Swift code and injected into the web page:
function sampleMethodTheHTMLCanCall( inputInfo, successFunc, errorFunc ) {
var promise = window.webkit.messageHandlers.namespaceWithinTheInjectedJSCode.postMessage( inputInfo );
promise.then(
function(result) {
console.log(result); // "Stuff worked!"
successFunc( result )
},
function(err) {
console.log(err); // Error: "It broke"
errorFunc( err )
});
}
And here is the page.html sample HTML that can call into the app code:
<html>
<meta name="viewport" content="width=device-width" />
<script>
function handleInfoFromApp( fromApp ){
document.getElementById("valToWrite").innerHTML = fromApp;
}
function handleError( err ){
}
</script>
<h1 id="valToWrite">Hello</h1>
<button onclick="sampleMethodTheHTMLCanCall( 'inputInfo', handleInfoFromApp, handleError )">Load Info from App</button>
</html>
The HTML above provides functions that will later get called by the app extension code upon success or failure of the javascript-initiated request.

Jason
- 1,323
- 14
- 19
-
This answer should be accepted in my opinion. Thank you very much!!!!! – biomiker Feb 17 '21 at 17:42
-
Hey @Jason I know this is a blast from the past, but I'm wondering if you might be able to explain why you added the script removal in the deinit func. I'm curious why that needs to happen. Thanks! – Xeaza Apr 07 '23 at 15:49