Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Call to swift method from JavaScript hangs xcode and application

I am writing an iOS App (using xcode 7.3 and swift 2.2) using JavascriptCode framework. Calling javascript methods from swift works perfect, but when I call the swift method from javascript, xcode simply shows a "loading" type of symbol and nothing happens. I need to "force quit" xcode to get out of this state. I have followed https://www.raywenderlich.com/124075/javascriptcore-tutorial and http://nshipster.com/javascriptcore/ and I am trying pretty simple calls.

Has anyone faced this kind of issue?

My swift code is as follows:

@objc protocol WindowJSExports : JSExport {
   var name: String { get set }
   func getName() -> String
   static func createWindowWithName(name: String) -> WindowJS
}

@objc class WindowJS : NSObject, WindowJSExports {
   dynamic var name: String
   init(name: String) {
       self.name = name
   }    
   class func createWindowWithName(name: String) -> WindowJS {
       return WindowJS(name: name)
   }    
   func getName() -> String {
       NSLog("getName called from JS context")
       return "\(name)"
   }
} 

I am initializing the context as follows:

runContext = JSContext()
runContext.name = "test_Context"

windowToJs = WindowJS(name: "test")
runContext.setObject(windowToJs.self, forKeyedSubscript: "WindowJS")

If I replace the last two lines in above code with below code without instantiating it, the code simply fails to load.

runContext.setObject(WindowJS.self, forKeyedSubscript: "WindowJS")

And the javascript code is as simple as

function check() {
    return WindowJS.getName()
}

I do see the breakpoint being hit in the JS function check and when the WindowJS.getName gets called, xcode simply becomes unresponsive.

like image 901
Amruta Avatar asked Jun 19 '16 14:06

Amruta


2 Answers

The setTimeout could be solved by adding following piece of code to my swift function.

let setTimeout: @convention(block) (JSValue, Int) -> () = 
{ callback, timeout in
    let timeVal = Int64(timeout)
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, timeVal), dispatch_get_main_queue(), { callback.callWithArguments(nil)})
}

To expose this native code to the JS context, I also added following.

runContext.setObject(unsafeBitCast(setTimeout, AnyObject.self), forKeyedSubscript: "setTimeout")

Things then worked fine.

like image 119
Amruta Avatar answered Sep 30 '22 03:09

Amruta


You're creating a deadlock since you are calling from Swift to JavaScript back to Swift. I'm not sure exactly why it is a deadlock but I had a similar issue with WKWebView on Mac recently.

You need to decouple this and make the communication asynchronous. This obviously means you cannot simply return a value from your JS function in this case.

To decouple, you can break the deadlock by deferring the work the JavaScript function needs to do out of the current runloop iteration using setTimeout:

function myFunction() {
  setTimeout(function() {
    // The actual work is done here.
    // Call the Swift part here.
  }, 0);
}

The whole native ↔︎ JavaScript communication is very, very tricky. Avoid it if you can. There's a project called XWebView that may be able to help you as it tries to ease bridging between the two worlds.

like image 29
DarkDust Avatar answered Sep 30 '22 03:09

DarkDust