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?
How does the iOS 14 API WKScriptMessageHandlerWithReply work for communicating with JavaScript from iOS?
Asked Answered
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.
This answer should be accepted in my opinion. Thank you very much!!!!! –
Aforethought
Hey @Mellon 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! –
Samara
Script message handlers are strongly retained by the WKWebView, so it's sometimes necessary to explicitly remove them in a
deinit
or similar. In this case: the ViewController retains the webView, and the webView strongly retains the view controller, so there is indeed a reference cycle that needs breaking. –
Immateriality © 2022 - 2024 — McMap. All rights reserved.