10

Is there any way to query the javascript synchronously from the main thread?

Javascript is queried from the native code using an asynchronous function with a callback parameter to handle the response:

func evaluateJavaScript(_ javaScriptString: String, completionHandler completionHandler: ((AnyObject!, NSError!) -> Void)?)

Asynchronous behavior can usually be turned synchronous by pausing the thread & controlling execution with a semaphore:

// Executing in the main thread
let sema = dispatch_semaphore_create(0)
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0)) {
// Background thread
  self.evaluateJavaScript("navigator.userAgent", completionHandler: { (value:AnyObject!, error: NSError!) -> Void in
    if let ua = value as? String {
      userAgent = ua
    } else {
      ERROR("ERROR There was an error retrieving the default user agent, using hardcoded value \(error)")
    }
    dispatch_semaphore_signal(sema)
  })
}
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER)

...however in this case, because the completionHandler is always called on the main thread, the code deadlocks because the completionHandler block will never execute (main thread was paused by the dispatch_semaphore_wait on the last line)

Any suggestions?

EDIT

I would rather not be blocking the main thread to execute that code. But I can't decouple from the main thread without changing my APIs from synchronous to asynchronous, with a domino effect all the way up the stack (e.g. from let ua = computeUserAgent() to computeUserAgent() {(ua: String)->Void in /*Use ua value here */}). So I need to choose between 2 approaches that both have downsides, and I would rather pick the approach that doesn't mess up my internal APIs, especially for a task as trivial as looking up the user agent.

Hugo
  • 886
  • 9
  • 19
  • Sure, here's a suggestion: Don't do that. Use the provided functionality and move on. Do not try to subvert asynchronous functionality. If you block the main thread waiting, the user cannot interact with your app and the Watchdog process may kill your app dead. There are legal ways of making the user unable to do anything temporarily, but this is not one of them. – matt Feb 07 '15 at 22:47
  • @matt, I think you are missing the point here: as I understand it, the fact that the completionHandler seems to always been run on the main thread makes it difficult to implement a synchronous abstraction of an evaluateJavascript call... Synchronous does not mean "on the main thread" – Stephane Philipakis Feb 07 '15 at 23:53
  • @StephanePhilipakis That isn't what he asked. He wants to this the main thread (see the title of the question) and be synchronous. He should not want that. My comment says "don't want that". – matt Feb 08 '15 at 00:06
  • @matt you are right, I did miss the point there :) – Stephane Philipakis Feb 08 '15 at 00:32
  • This said, I'm not sure I understand why the evaluateJavaScript's callback execution has to be on a predefined thread. seems like an unnecessary constraint on the WK API side... – Stephane Philipakis Feb 08 '15 at 00:42
  • @matt Thanks for the comment. Agree I should not want to block the main thread for the purpose of getting the user agent... ideally the `user agent` would be a property of the WKWebView, and all of that nonsense would go away! I edited the question to provide some background. – Hugo Feb 08 '15 at 03:22
  • "and I would rather pick the approach that doesn't mess up my internal APIs" Well I am suggesting that you're picking wrong. In my view, you must accept the asynchronous nature of `evaluateJavaScript:completionHandler:` and break your calling method in two. Or use UIWebView, since `stringByEvaluatingJavaScriptFromString:` is not asynchronous. If you have a use case, file an enhancement request (WKWebView has lots of huge feature holes, in my opinion), but do not try to screw with the threading. – matt Feb 08 '15 at 03:56
  • 2
    So I'm wrapping a web application with native code to provide persistence and access to the system clipboard. My data model therefore lives in JavaScript. When its time to save, or export, or copy, or cut, or print, I need to be able call JavaScript in response to that action and *get a result back*. Specifically, for export and print, I have to get that result in the middle of a "UI" operation. If there is a way to a) do this asynchronously or b) do this outside of the main thread, I don't see it. Suggestions? – M. Anthony Aiello Mar 31 '15 at 16:48
  • @M.AnthonyAiello I have the exact same problem. My data model lives in the WKWebView and I need synchronous access to the data model during a UI operation. Did you ever find a solution? – Mark Apr 08 '21 at 07:44

1 Answers1

3

If you must do this...

As suggested in a comment to this answer you could run a tight loop around your semaphore wait like this.

while (dispatch_semaphore_wait(sema, DISPATCH_TIME_NOW)) { 
    [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode 
                             beforeDate:[NSDate dateWithTimeIntervalSinceNow:10]];
}
Community
  • 1
  • 1
Tristan Burnside
  • 2,356
  • 1
  • 13
  • 23
  • Kludgy but it's a working answer to the question thank you for that! Not quite yet sure what I will use for my project (In particular, @matt makes some good points in the question's comment section) but I will accept this answer tomorrow unless a better one comes through – Hugo Feb 08 '15 at 05:32
  • Hello. I tested your solution and it is not working. The completionHandler is still blocked and still not executed :/ – h3dkandi Apr 21 '16 at 14:39
  • it can cause crash! – Nikita Dec 15 '16 at 12:31