Using setInterval, setTimeout in JavascriptCore Framework for ObjectiveC
Asked Answered
F

4

5

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.

  1. Am I right about this?
  2. Are there alternatives?

I could potentially create the timer in Objective C but most of my library is written in Javascript, and aside from that, it seems just weird not having setInterval or setTimeout!!

I've tried a few alternative methods:

window.setInterval(function(){ /* dosomething */ }, 1000);
setInterval(function(){ /* dosomething */ }, 1000);

var interval;
interval = window.setInterval(function(){ /* dosomething */ }, 1000);
interval = setInterval(function(){ /* dosomething */ }, 1000);

I have no way to monitor what's even happening in the JSVirtualMachine either. All I know is my code stops working when there is a setInterval called.

Any help super appreciated!

Forth answered 13/1, 2014 at 19:55 Comment(2)
I have not tested setInterval specifically but you can have a javascript function call objective-c code like the examples show through blocks. You would be able to tell if there was really a timeout that way.Krystenkrystin
@Krystenkrystin Hi there! That's exactly what I'm doing I think (trying to execute NSLog in a function called by setInterval). It looks like setInterval and setTimeout do not exist in this framework. I will leave this question open until someone can confirm it. If they can (along with their method of confirming) I will accept their answer!Forth
C
14

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. :)

Clemenceau answered 5/10, 2016 at 1:54 Comment(11)
Did miss the callJsCallback function?Consulate
Does not compile, missing callJsCallback method.Mcclish
I'm sorry, just saw the comments now. Kind of a long time ago. It's actually just a call to the callback defined as userInfo. I've edited the answer.Ordinate
@JoséCoelho It's still broken... Argument of '#selector' refers to instance method 'callJsCallback' that is not exposed to Objective-CSoothsay
These timers can fire much faster than they would in JavaScript. For example, browsers usually throttle timeouts to 100ms, and the callback will never be invoked until at least the next event loop after resolved Promise callbacks run. These callbacks however can be evaluated before Promises that resolved ahead of calling setTimeout.Heard
clearTimeout need to also run in a queue otherwise timers.removeValue can be evoked before the timer is created and added to timers.Heard
Excellent answer! I fixed some errors (like callJsCallback not being marked with @objc). I also moved everything to queues to avoid race conditions and added the clearInterval method which is needed by some libraries. You can find my improved version here gist.github.com/heilerich/e23cfc6fe434919de972140147f83f6fKnowledgeable
@Knowledgeable , How I can use this class you have shared in the gist link. Can you share the usage example for that. I already having a function where I have a JSBundle in context.evaluateScript . Now how to add this to my existing jscontext object.?Titicaca
Whn doing below thng , i am getting error . Any suggestion let jsContext = JSContext() JSTimer.registerInto(jsContext: JSContext); --> Error on this line "Cannot convert value of type 'JSContext.Type' to expected argument type 'JSContext'"Titicaca
@rahul goyal I think you are misspelling the name of your variable in the register function argument. It should be jsContext not JSContext (mind the lower case js prefix ). I would suggest choosing less ambiguous variable names.Knowledgeable
@Knowledgeable , One more doubt I have created One Webpack bundle and I have created this SetTimeout function. Now when the setTimeout() is called from inside the bundle , control is reaching in our swift callback function , but callback function is not getting executed. If I call SetTimeout() function from outside the bundle code it will execute callback but not able to do it if it called from inside the bundle . Seems some kind of scope issue. Do you any idea how we can resolved this problem?Titicaca
E
8

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.

Endodermis answered 29/12, 2015 at 23:13 Comment(3)
Very thoughtfully laid out answer. I don't know if it works, but great job.Surface
The answer is great, but the order of the arguments is wrong, it should be (JSValue *callback, double ms) in order to be window.setTimeout compatible. I'd also suggest to use dispatch_after to avoid any retain cycles and have it more compact.Snorter
Thank you for your answer, however I've encountered a problem with this code, I called setTimeout with closure and the second time call won't run, sample code may like: var times = 5; var fn = () => { setTimeout(() => { console.log('123'); if (times-- > 0) fn(); }, 500); };fn(); The fn function called and logged 123 for once, and following fn() call won't execute, I'm wondering why, could you help?Convoluted
K
2

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.

Krystenkrystin answered 16/1, 2014 at 19:12 Comment(2)
Wow that's way beyond what I even asked...! I just found a way to call what I needed to call using Objective C Timers. But E for Effort! (or 14... whatever) :) It's a shame these functions are not included in the core. They seem like integral parts of javascript to me and are normally included in other JS Virtual Machine type things (ie. Web Workers etc.)Forth
Nope, they are not, since they are not are part of ecma-262 spec, that is, not a part of language.Reservoir
A
0

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?

Alphabetic answered 24/8, 2014 at 3:48 Comment(3)
UIWebView isn't the same as JavascriptCore.Forth
Ah, my mistake. I haven't seen any use for JavascriptCore except to interface with UIWebView. What were you trying to do?Alphabetic
There are many javascript libraries that do many useful things not DOM related! :) I was trying to port one of my own for iOS with minimal work and it all worked except I needed to find a way to do a setInterval. I just ended up using a native objC timer to call the method every x seconds.Forth

© 2022 - 2024 — McMap. All rights reserved.