Call a JS function from Obj-C and pass an Obj-c callback/function? #197

Closed
SRandazzo opened this Issue Jun 26, 2013 · 11 comments

Projects

None yet

4 participants

@SRandazzo
Contributor

When calling a function on JS from Objective-c, is there anyway to pass an objective-c block/callback into JS?

for example:

[self.jsView evaluateScript:@"myAsyncFunction(123, myCallback())"];
@guileen
guileen commented Jun 27, 2013

You can take a look #193

@SRandazzo
Contributor

Thanks but #193 looks like it's talking about executing JSCallbacks from Obj-c (If i'm understanding it correctly)

I want to pass an objective-c block/callback into JS and let the JS execute it

@phoboslab
Owner

In your objc binding:

EJ_BIND_FUNCTION(callFunction, ctx, argc, argv) {
    JSObjectRef jsCallback = (JSObjectRef)argv[0];

    JSObjectRef objcCallback = [scriptView createFunctionWithBlock:^JSValueRef(JSContextRef ctx, size_t argc, const JSValueRef *argv)
    {
        NSLog(@"block called from JS with value: %@", JSValueToNSString(ctx, argv[0]));
        return NULL;
    }];

    JSValueRef params[] = { objcCallback };
    [scriptView invokeCallback:jsCallback thisObject:NULL argc:1 argv:params];
    return NULL;
}

In JavaScript:

myBindingInstance.callFunction(function(objcCallback){
    objcCallback("zomg");
    objcCallback("foobar");
});

This should help a bit, I hope :)

@phoboslab phoboslab closed this Jul 9, 2013
@SRandazzo
Contributor

@phoboslab

createFunctionWithBlock:^JSValueRef(JSContextRef ctx, size_t argc, const JSValueRef *argv)

O_o

whoaaaaa, awesome! thanks a ton, this is great

@SRandazzo
Contributor

@phoboslab this is really great so far, but how can i execute this from outside the scope of a JS function being called?

For example from anywhere in my obj-c code If i want to tell the js view
[self.jsView evaluateScript:@"doSomething()" params:jsParams];

@phoboslab
Owner

Not sure I understand. You want to call the JS function doSomething() and pass in an obj-c block function?

If doSomething() is global (which it is, if you can invoke it with evaluateScript), you can get a reference to it like this:

JSObjectRef global = JSContextGetGlobalObject(ctx);

JSStringRef jsFuncName = JSStringCreateWithUTF8CString("doSomething");
JSObjectRef jsFunc = (JSObjectRef)JSObjectGetProperty(ctx, global, jsFuncName, NULL);
JSStringRelease(jsFuncName);

// ...
[scriptView invokeCallback:jsFunc thisObject:NULL argc:1 argv:params];

(The ScriptView's invokeCallback method should probably be renamed to invokeFunction, or something)

@SRandazzo
Contributor

Very helpful! Seems like we could even wrap some of that up in Obj-C

While it works if I call the function doSomething that i have implemented, when i try to use it on other global objects, for example PP.api.doSomething it does not execute :-/

If it helps, I'm also using the jsview.jsGlobalContext as the ctx, so:

JSObjectRef global = JSContextGetGlobalObject(  self.jsView.jsGlobalContext  );

JSStringRef jsFuncName = JSStringCreateWithUTF8CString("PP.api.doSomething");
JSObjectRef jsFunc = (JSObjectRef)JSObjectGetProperty( self.jsView.jsGlobalContext , global, jsFuncName, NULL);
JSStringRelease(jsFuncName);

Invoking that above does nothing, in my js i have:

API.prototype.doSomething = function () {
    console.log('DO SOMETHING');
    return null;
}

and then we have a

PP.api = new API({.....
@phoboslab
Owner

PP.api.doSomething is not a global object, PP is. api is a property of PP and doSomething is a property of api. You have to call JSObjectGetProperty for each name recursively.

I'd propose a method like this on the scriptView (untested):

- (JSValueRef)getValueForPath:(NSString *)objectPath {
    JSValueRef obj = JSContextGetGlobalObject( jsGlobalContext  );

    NSArray *pathComponents = [objectPath componentsSeparatedByString:@"."];
    for( NSString *p in pathComponents) {
        JSStringRef name = JSStringCreateWithCFString((CFStringRef)p);
        obj = JSObjectGetProperty( jsGlobalContext, (JSObjectRef)obj, name, NULL);
        JSStringRelease(name);

        if( !obj ) { break; }
    }
    return obj;
}

//...
JSObjectRef func = (JSObjectRef)[scriptView getValueForPath:@"PP.api.doSomething"];

Let me know how that goes; this method would probably be a good addition to Ejecta.

@SRandazzo
Contributor

That works perfect!

Definitely a worthwhile addition to the project, really rounds out the communication from Objc-C to JS!

thanks again for all the help! I can gladly PR back changes if you like

@finscn
Contributor
finscn commented Jul 17, 2013

I think it's named getValueByPath better than getValueForPath ;)

@phoboslab phoboslab added a commit that referenced this issue Jul 18, 2013
@phoboslab Added jsValueForPath. See #197 290930b
@phoboslab
Owner

Renamed the method to jsValueForPath. The "for" is more consistent with Apple's API names, e.g. NSDictionary valueForKey.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment