Search code examples
swiftwkwebviewjavascriptcore

Async communication of swift and Javascriptcore


I want to use the Async functionality offered by WKWebView outside web view. The JS Context option does not provide the Async functionality.

In WKWebView, I write my logic as follows.

func swiftFunc1() {
   webView.evaluateJavaScript("jsFunc1(), completionHandler: nil)
}

In javascript code I post a message to swift

function jsFunc1() {
    window.webkit.messageHandlers.myMsg.postMessage("call swiftFunc2");
}

The swift code can then call appropriate JS callback as a part of handling message.

But this is dependent upon the webview being the foreground view. If I want to use the JS logic independent of the webview, JSContext is the option. I tried following

func swiftFunc1() {
    myCtxt = JSContext()
    exportedToJS = exportToJS() //confirms to JSExport and uses @objc
    myCtxt.setObject(exportedToJS.self, forKeyedSubscript: "swiftIface")
    myFunc = myCtxt.objectForKeyedSubscript("jsFunc1")
    myFunc.callWithArguments(nil)
}

Now in javascript code I cannot post a message to swift. If I try to call a swift function as follows, code gets stuck forever.

function jsFunc1() {
   swiftIface.swiftFunc2() // This creates a deadklock
 }

How can I achieve either of the following without "returning" from the called Javascript function jsFunc1()?

  1. Either post a message to swift so that it can take appropriate action

  2. Or call a swift function so that the appropriate action is taken


Solution

  • Do I understand you right, if you do not want your javscript to terminate after execution?

    If I understood you wrong, maybe following helps (I am not at home at Mac to test, but maybe it works if you modify your code as follows).

    Option 1: Blocks

    The swiftFunc1 could look like this:

    func swiftFunc1() {
        myCtxt = JSContext()
        myCtxt.setObject(unsafeBitCast(swiftFunc2, AnyObject.self), forKeyedSubscript: "swiftFunc2")
        exportedToJS = exportToJS() //confirms to JSExport and uses @objc
        myCtxt.evaluateScript("swiftFunc2()")
    }
    

    Your swiftFunc2 would look like this:

    let swiftFunc2: @convention(block) Void -> Void = { 
      // do something here
    }
    

    Your JS code would look like this:

    function jsFunc1() {
       swiftFunc2();
     }
    

    Option 2: JSExport Your have an exported class which is accessible for all javascript:

    import Foundation
    import JavaScriptCore
    @objc class JavascriptHandler: NSObject, JavascriptHandlerExport {
      let context: JSContext = JSContext()
    
      init () {
        context.setObject(self, forKeyedSubscript: "MyJSHandler") // set the object name for self accessible in javascript code
      }
      func swiftFunc1() {
        context.evaluateScript("MyJSHandler.swiftFunc2();")
      }
      func swiftFunc2 () {
        // do something here
      }
    }
    

    Your protocol for the exported class.Here you have to declare all properties and methods you want to use with Javascript.

    import Foundation
    import JavaScriptCore
    @objc protocol JavascriptHandlerExport: JSExport {
      func swiftFunc2 ( ) -> Void
    }
    

    With this it should be possible for you to call a function from javascript and still let it continue. You can now access the functions of the class JavascriptHandler from Javascript like this in this example:

    MyJSHandler.swiftFunc2();
    

    If you want to seperate the class where your WebView is from the one where your JS logic lies that should also not be a problem. You should also be able to combine the block syntax with the JSExport method.

    Let me know if it's not working/behaving as wanted.