Nativescript: [Feature Request] TS Decorators for JS -> Obj-C bridge

Created on 19 Apr 2017  路  11Comments  路  Source: NativeScript/NativeScript

Looking at the @Interfaces(...) decorator provided for android, wouldn't it be possible to implement decorators for the iOS side as well (where you can specifie protocols, exposed methods, etc.)?

Example

@ObjCProtocols([NSCoding])
class JSObject extends NSObject implements NSCoding {
    public encodeWithCoder(aCoder) { /* ... */ }

    public initWithCoder(aDecoder) { /* ... */ }

    @ObjCMethod([interop.types.void]) // This is the return type
    public "selectorWithX:andY:"(@ObjCParam([interop.types.id]) x, @ObjCParam([interop.types.id]) y) { /* ... */ }
}
feature help wanted ios

Most helpful comment

@PanayotCankov , I think those will help, because right now whenever I have to expose something (especially the methods) I have to search through the docs to find the correct format of the static properties 馃槃

So such helper functions should be added to the ios-runtime report? What would be a good place for those, may be https://github.com/NativeScript/ios-runtime/blob/master/src/NativeScript/inlineFunctions.js ? If so may be I should move this issue to the ios-runtime repo. I might play with those if I find some time 馃槃

All 11 comments

This was closed due to a lack of interest: https://github.com/NativeScript/ios-runtime/issues/552 but is something we've discussed internally in the past. It turned out that mutating class decorator can actually use the ".extend" static method on the base class and return the generated constructor function in-place of the original one seamlessly.

@PanayotCankov , I think those will help, because right now whenever I have to expose something (especially the methods) I have to search through the docs to find the correct format of the static properties 馃槃

So such helper functions should be added to the ios-runtime report? What would be a good place for those, may be https://github.com/NativeScript/ios-runtime/blob/master/src/NativeScript/inlineFunctions.js ? If so may be I should move this issue to the ios-runtime repo. I might play with those if I find some time 馃槃

Well, here are my two cents:

The class decorator should support everything that can be passed to the .extend method. Otherwise the JavaScript version of the extend will be more powerful than the TypeScript decorators based version and eventually there will be scenarios that are not well supported by the TypeScript well.

Having multiple decorators on the same class clutters the syntax and may introduce constraints on the order in which the decorators should be placed on the class so it is best to have a single decorator rather than multiple ones (e.g it would be best to have @java(name: string, ... interfaces: any[]) rather than @Name and @Interfaces.

Method and property decorator are called before the class decorator so you can aggregate all data about exposed methods in iOS before the class decorator is called, and then when the class decorator is called take use of it.

It would be best if the decorators API is designed by a JavaScript/TypeScript developer and implemented in by the runtimes. That way we will have the most appealing public API and the implementation and in the runtimes will be done by the people that have the best domain knowledge about the JSC, V8, marshalling etc. This is also IMO the scenario that is most unlikely to happen. Anyway you can make the decorators just call .extend on the base class and it will be ok.

The methods names defined for iOS better avoid the ":" character as it will make the method hard to call from JavaScript.

The types and implemented protocols will rarely vary at runtime so it would be better to get them as rest args and save the extra array ([]) symbols.

The parameter decorators seem somewhat fine but way too verbose. Instead of having @ObjCType(type: intoropType), you can make a decorator for each of the interop types. E.g instead of @ObjCParam(interop.types.id), @ObjCParam(interop.types.int) etc. expose something like @id, @int.

Consider reflect-metadata and TypeScript's "emitDecoratorMetadata": true. Last time I checked it was possible to use it to obtain some names for parameter types, but a number was still just a number so these probably won't help much.

So with these considerations in mind the public API could look something like this:

@java("org.nativescript.Button", InterfaceA, InterfaceB)
class Button extends android.widget.Button implements InterfaceA, InterfaceB {
}
@objc(ProtocolA, ProtocolB)
class Button extends UIView implements ProtocolA, ProtocolB {
    @objc("selectorWithX:andT:", void)
    selectorWithXandY(@objc.int x: number, @objc.int y: number) {
        console.log(`${x}, ${y}`);
    }
}

@PanayotCankov , I like your API proposal! One question though, for the methods, if we expose a method with colons, but the JS method does not have the colons in its name, will the {N} framework automatically pickup and use the method when called from native? Or the decorator will have to add to the prototype the name with the colons?

If it has no colons, the {N} won't automatically pick it up in the native. This mapping can't be expressed in JavaScript so don't worry about it. Make the method decorator accept just the return type.

In the runtime there are method-meta objects that keep ObjC to JS mapping and when classed are exposed these metas are generated at compile time. For classes extended from JavaScript these metas are generated dynamically and currently the JS and ObjC names match. You can name the method "selectorWithXandY" in JavaScript and use this string as selector for the action of "addTarget:action:forControlEvent:" for example and ObjC won't complain. You can name the method "selectorWithX:andY:" and pass that string as selector for "addTarget:action:forControlEvent:" and again from ObjC it would be able to call it, but it will be a little harder to call it from JavaScript. So ObjC will work in both cases but it would be a little strange to have selector without colons that accepts arguments.

@PanayotCankov , sadly there are problems with implementing the desired API.

  1. Seems that we cannot call .extend directly on the target as the runtime complains that it is not getting called on a native class. So we need there the base type, and from what I saw there is no way to get it.
  2. For the parameters, seems in many cases a non-interop type is being used (NSString, NSObject, etc.) So we really need to pass this as parameter. If there is need for shorthand decorators for the interop types, those could be added.
  3. Without calling .extend from what I see there is no way to specify a custom name for the class

With this said, currently the public API looks like:

@ObjCProtocols(Protocol1, Protocol2)
class JSObject extends NSObject implements Protocol1, Protocol2{
    public encodeWithCoder(aCoder) { /* ... */ }

    public initWithCoder(aDecoder) { /* ... */ }

    @ObjCMethod("selectorWithX:andY:", interop.types.void) // return type optional
    public selectorWithX:AndY(@ObjCParam(interop.types.id) x, @ObjCParam(NSString) y) { /* ... */ }
}

@PeterStaev Great job! Pragmatic and effectively closes the gap between the iOS and Android inheritance in TS a little.

On your points:

  1. The target is a constructor function and it has prototype, this prototype is created from the base class and its __proto__ is the prototype of the base class, which has constructor property that points to the base class. So it is like:
function objc(... protocols:any[]): ClassDecorator {
    return function ext(target: Function): Function {
        var baseTarget = Object.getPrototypeOf(target.prototype).constructor;
        var proto = {};
        for (var name in Object.getOwnPropertyNames(target.prototype)) {
            proto[name] = target.prototype[name];
        }
        // TODO: Pass protocols and inspect methods for exposed methods
        var derivedCtor = baseTarget.extend(proto);
        // TODO: Copy similar to the TypeScript's __extend methods from the base ctor to derivedCtor.
        // TODO: Copy form target to derivedCtor static methods and properties
        return derivedCtor;
    }
}

@objc()
class A extends NSObject {
}
  1. These are just "id" types. @ivanbuhov right? They are used in figuring out objc msg send calls.
  2. goto 1.

Hi,
@ivanbuhov noted the Object.getPrototypeOf(target) will work too. The modern JavaScript and the runtime wires the prototype chain of the constructor functions properly, not only the prototype chain of the prototype objects. This is so cool. :+1:

This issue was moved to NativeScript/ios-runtime#754

This thread has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

guillaume-roy picture guillaume-roy  路  3Comments

NickIliev picture NickIliev  路  3Comments

rLoka picture rLoka  路  3Comments

nirsalon picture nirsalon  路  3Comments

valentinstoychev picture valentinstoychev  路  3Comments