Returning a Promise from WebView's evaluateJavascript?


#1

I’m trying to run JavaScript in a WebView to pull some data out of a single-page app and I’m running into an issue with asynchronous code, stuff like MutationObserver and anything that returns a Promise: is there any way to return an asynchronous result from evaluateJavaScript?

In Shortcuts, it’s possible to do this because it requires you to call a completion handler with the result and the shortcut waits until that handler gets called. Does Scriptable have anything similar?


#2

Scriptable doesn’t currently have anything similar. Evaluating JavaScript in Scriptables WebView uses WKWebViews support for evaluating JavaScript under the hood.

However, it’s an interesting use case that I’d like to look into for a future update.


#3

I ran into the same problem this weekend: I tried to offload timer functions, aka setTimeout and setInterval to an unrendered web view (I may or may not have been trying to find a solution to this :sunglasses:), but it fails whenever I return the Promise I wrapped them in to the main Scriptable script.

It would be really useful to be able to handle async scripting in Scriptable’s WKWebView wrapper; also, an implementation of the messaging mechanisms between WKWebView JS and its caller would be welcome (it is doable, although I have absolutely no idea how easily: JSBoxWeb API, another WKWebView wrapper for JS, includes two way messaging).


#4

The latest beta build supports running asynchronous operations in the web view and pass back a result. For example, you can do the following to offload setTimeout to the web view.

let webView = new WebView()
let js = `
setTimeout(function() {
  completion(null)
}, 3000)
`
log("Waiting three seconds...")
let result = await webView.evaluateJavaScript(js, true)
log("Time's up!")

It’s in the latest beta build and will be available on the App Store soon.


#5

@simonbs yeah, I’ve noticed that in the release notes, great news. You are doing awesome work on Scriptable.

With the newest beta’s support for asynchronous operations in WebViews, the solution I cobbled together for setTimeOut functionality actually works. The following class:

class Timer {
  constructor () {
    this.view = new WebView()
    let html = '<script>function wait (ms) { setTimeout(completion, ms) }</script>'
    this.ready = this.view.loadHTML(html)
  }
  
  async add (delay) {
    let target = Date.now() + delay
    await this.ready
    let remain = target - Date.now()
    return remain > 0 ? this.view.evaluateJavaScript(`wait(${remain})`, true) : Promise.resolve()
  }
}

will give you a Timer object that allows you to generate timer promises. For instance, doing this:

const timer = new Timer

const first = timer.add(2500)
first.then(() => console.log('First countdown ended'))
  
const second = timer.add(5000)
second.then(() => console.log('Second countdown ended'))

await first
await second

will log after, respectively, 2.5 and 5 seconds. The advantage of this construct over Simon’s example is that with only one WebView, you can set several timers that will run in parallel – without the function embedded in the HTML, you need one WebView per, which is expensive when it comes to memory (setting multiple timers without the embedded function will simply block the script). There is a downside, which is that the HTML has to load before the first timer will run, hence the await and remain shenanigans in the add function; on my iPad Air 2, loading HTML into a WebView the first time takes 1 to 1.5 seconds, so it’s not the right solution if all you need is one short timeout below that threshold. EDIT: I found that if I load only the bare <script> tag instead of a whole HTML document, loading delay is around 200 ms (presumably because that way loading does not trigger DOM construction?), so there is no real downside unless you absolutely need a shorter timeout than that.

I recommend making a module out of the class rather than copy-pasting the code, i.e. wrapping it in

const exports = {
  Timer: // <class code above>
}

storing it in its own file and loading it with my require shim.


Problem running Dictation.start() more than once in a script
setTimeout missing