How does the iOS 14 API WKScriptMessageHandlerWithReply work for communicating with JavaScript from iOS?
Asked Answered
M

1

8

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?

Mellon answered 12/12, 2020 at 21:39 Comment(0)
M
19

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.

Mellon answered 12/12, 2020 at 21:39 Comment(3)
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.