Skip to content Skip to sidebar Skip to footer

Using SetInterval, SetTimeout In JavascriptCore Framework For ObjectiveC

I've been experimenting with the new Objective C JavascriptCore Framework. This is crazy, but it seems that it doesn't have setTimeout or setInterval. Which... I don't understand.

Solution 1:

A new implementation solving an old question.

setTimout, setInterval and clearTimeout are not available on the context of JavaScriptCore.

You need to implement it by yourself. I've created a swift 3 class to solve this problem. Usually, the examples show only the setTimeout function without the option to use clearTimeout. If you are using JS dependencies, there's a big chance that you are going to need the clearTimeout and setInterval functions as well.

import Foundation
import JavaScriptCore

let timerJSSharedInstance = TimerJS()

@objc protocol TimerJSExport : JSExport {

    func setTimeout(_ callback : JSValue,_ ms : Double) -> String

    func clearTimeout(_ identifier: String)

    func setInterval(_ callback : JSValue,_ ms : Double) -> String

}

// Custom class must inherit from `NSObject`
@objc class TimerJS: NSObject, TimerJSExport {
    var timers = [String: Timer]()

    static func registerInto(jsContext: JSContext, forKeyedSubscript: String = "timerJS") {
        jsContext.setObject(timerJSSharedInstance,
                            forKeyedSubscript: forKeyedSubscript as (NSCopying & NSObjectProtocol))
        jsContext.evaluateScript(
            "function setTimeout(callback, ms) {" +
            "    return timerJS.setTimeout(callback, ms)" +
            "}" +
            "function clearTimeout(indentifier) {" +
            "    timerJS.clearTimeout(indentifier)" +
            "}" +
            "function setInterval(callback, ms) {" +
            "    return timerJS.setInterval(callback, ms)" +
            "}"
        )       
    }

    func clearTimeout(_ identifier: String) {
        let timer = timers.removeValue(forKey: identifier)

        timer?.invalidate()
    }


    func setInterval(_ callback: JSValue,_ ms: Double) -> String {
        return createTimer(callback: callback, ms: ms, repeats: true)
    }

    func setTimeout(_ callback: JSValue, _ ms: Double) -> String {
        return createTimer(callback: callback, ms: ms , repeats: false)
    }

    func createTimer(callback: JSValue, ms: Double, repeats : Bool) -> String {
        let timeInterval  = ms/1000.0

        let uuid = NSUUID().uuidString

        // make sure that we are queueing it all in the same executable queue...
        // JS calls are getting lost if the queue is not specified... that's what we believe... ;)
        DispatchQueue.main.async(execute: {
            let timer = Timer.scheduledTimer(timeInterval: timeInterval,
                                             target: self,
                                             selector: #selector(self.callJsCallback),
                                             userInfo: callback,
                                             repeats: repeats)
            self.timers[uuid] = timer
        })


        return uuid
    }

    func callJsCallback(_ timer: Timer) {
        let callback = (timer.userInfo as! JSValue)

        callback.call(withArguments: nil)
    }
}

Usage Example:

jsContext = JSContext()
TimerJS.registerInto(jsContext: jsContext)

I hope that helps. :)


Solution 2:

I know I am responding to an old question, but as I have been porting JavaScriptCore to Android, I came across this issue.

setTimeout and setInterval are not part of pure JavaScript. They are a part of the DOM. Just like document and xmlHttpRequest are not available in JavaScriptCore, either. JavaScript was designed to be a single-threaded, synchronous execution environment. A call is made from the host, the code is executed, and then returns. That's all it does. Any other magic that happens is supported by the host.

So to support it in your code, you must implement it on the host side. This is easily done as follows:

context[@"setTimeout"] = ^(int ms, JSValue *callback) {
    [NSTimer scheduledTimerWithTimeInterval:ms/1000
        target:[NSBlockOperation blockOperationWithBlock:^{
            [callback callWithArguments:nil];
        }]
        selector:@selector(main)
        userInfo:nil
        repeats:NO
    ];
}

You need to be careful, though. As mentioned, JS has no semaphore protection or anything of the sort. To implement this correctly, you should create a single execution thread:

dispatch_queue_t jsQueue = dispatch_queue_create("com.some.identifier",
    DISPATCH_QUEUE_SERIAL);

And then when executing your code, always run it in this queue:

dispatch_async(jsQueue, ^{
    [context executeScript:myJSCode];
});

Finally, you can rewrite the above setTimeout function as follows:

context[@"setTimeout"] = ^(int ms, JSValue *callback) {
    NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:ms/1000
        target:[NSBlockOperation blockOperationWithBlock:^{
            dispatch_async(jsQueue, ^{
                [callback callWithArguments:nil];
            });
        }]
        selector:@selector(main)
        userInfo:nil
        repeats:NO
    ];
};

Now it is thread-safe and will do what you expect.


Solution 3:

I did more looking and fiddling and it seems like neither of them are part of javascriptCore. I made a function that returned something and if it returned null then there was some code error inside it.

Anyway, I also tried to then recreate the timeout functions in objective c but the problem comes when trying to pass the function/closure into the timeout. It appears from what I've seen you cannot pass that sort of information through. You get a string back. At any point if in the code local variables are being referenced it wouldn't work.

I can imagine you could make a workaround where the timer acts as a delayed messenger where you send the timer an identifier of some sort and after the delay it sends the string back to be ran by some other class that uses the id to call the right thing.

var interval = setInterval( 1000 );
someObj[interval] = someFnToCall;
... 
someObj[interval]();
...

[edit] Did some work to replicate the setTimeout properly. setInterval would work pretty much the same way since you could invalidate the timer by nilling out the value in callbackstore

var callbackstore = {};

var setTimeout = function( fn, ms ) {
  callbackstore[setTimeoutFn(ms)] = fn;
}

var runTimeout = function( id ) {
  if( callbackstore[id] )
    callbackstore[id]();

   callbackstore[id] = nil;
}

and the objective-c side looks like this

JSValue *timeOutCallback = self.context[ @"runTimeout" ];

self.context[@"setTimeoutFn"] = ^( int ms ) {
  NSString *str = @"xyz";

  dispatch_after(dispatch_time(DISPATCH_TIME_NOW, ms * NSEC_PER_MSEC), dispatch_get_main_queue(), ^{
    [timeOutCallback callWithArguments: @[str]];
  });

  return str;
};

xyz was just to verify, it should be some incrementing string of some sort that would be valid as a javascript object name.

This properly maintains all the nice closure stuff javascript likes.


Solution 4:

I know you've accepted an answer, but I'm unsure why setTimeout doesn't appear to be available to you.

This works fine for me after I've grabbed the JSContext from the UIWebView.

JSValue *callback = self.jsContext[@"someFunctionInJS"];
callback.context[@"setTimeout"] callWithArguments:@[callback, @(0)];

This results in execution of the callback. What are you trying to achieve, exactly?


Post a Comment for "Using SetInterval, SetTimeout In JavascriptCore Framework For ObjectiveC"