NativeScript Angular

Subclassing Objective-C Classes

For Objective-C classes we have JavaScript constructor functions and for Objective-C protocols we have objects. They can be used to subclass an Objective-C class or implement Objective-C protocol from JavaScript.

Subclassing an Objective-C Class

The constructor functions have a static method called extend used to declare Objective-C subclasses from JavaScript.

Implementing Protocols

To implement a protocol use the extend method on an Objective-C class, the implemented protocols are provided in the extend arguments.

Extend

The extend method has the following usage:

var <DerivedClass> = <BaseClass>.extend(classMembers, [nativeSignature]);

classMembers

The properties of the classMembers argument define the instance class members:

  • methods - define or override instance methods
  • properties - define or override instance properties

There are three type of methods, which can be contained in this object - base class overrides, native visible methods and pure JavaScript methods. The difference between native visible and pure Javascript methods is the that later are only accessible in your JavaScript code. Should you want the method to be visible and callable from the native libraries, you should provide a second parameter to extend. This parameter should provide the needed additional metadata about the method signature.

For more information how Objective-C method names are mapped to JavaScript property names check the "Methods" section in "Objective-C Classes".

To override Objective-C properties you have to use JavaScript getters/setters (ECMA 5). If the property isn't read-only, you have to override both the getter and the setter.

This parameter of extend is set as prototype to the instances created by the constructor. You shouldn't reuse it for other extend calls.

nativeSignature

The nativeSignature argument is optional, it has the following properties:

  • name - optional, string with the derived class name
  • protocols - optional, array with the implemented protocols
  • exposedMethods - optional, dictionary with method names and native method signature objects

The exposedMethods property defines the Objective-C signatures of new methods, this is usually required from delegates and APIs that expect target with selector pairs. Methods that are overridden will infer their signatures from the base class or protocols they implement.

NOTE: When exposing a new method to the Objective-C runtime (as opposed to overriding an existing method), its name in both the classMembers object and exposedMethods object has to be the exact same string. This string is also used as the selector of the method in Objective-C.

The native method signature object has two properties:

  • returns - required, type object
  • params - required, an array of type objects

The type object in general is one of the runtime types:

  • A constructor function, that identifies the Objective-C class
  • A primitive types in the interop.types object
  • In rare cases can be a reference type, struct type etc. described with the interop API

Calling Base Methods

Calls to native base class methods in overrides are in the form:

<BaseTypeName>.prototype.<MethodName>.apply(this, arguments);

or

this.super.<MethodName>(<arguments>);

Getting or setting properties using the base getters and setters is possible through the super property.

Subclass Example

The following example subclasses the UIViewController:

var MyViewController = UIViewController.extend({
    // Override an existing method from the base class.
    // We will obtain the method signature from the protocol.
    viewDidLoad: function () {
        // Call super using the prototype:
        UIViewController.prototype.viewDidLoad.apply(this, arguments);
        // or the super property:
        this.super.viewDidLoad();

        // Add UI to the view here...
    },
    shouldAutorotate: function () { return false; },

    // You can override existing properties
    get modalInPopover() { return this.super.modalInPopover; },
    set modalInPopover(x) { this.super.modalInPopover = x; },

    // Additional JavaScript instance methods or properties that are not accessible from Objective-C code.
    myMethod: function() { },

    get myProperty() { return true; },
    set myProperty(x) { },
}, {
    name: "MyViewController"
});

Protocol Implementation Example

The following example implements the UIApplicationDelegate protocol:

var MyAppDelegate = UIResponder.extend({
    // Implement a method from UIApplicationDelegate.
    // We will obtain the method signature from the protocol.
    applicationDidFinishLaunchingWithOptions: function (application, launchOptions) {
        this._window = new UIWindow(UIScreen.mainScreen.bounds);
        this._window.rootViewController = MyViewController.alloc().init();
        this._window.makeKeyAndVisible();
        return true;
    }
}, {
    // The name for the registered Objective-C class.
    name: "MyAppDelegate",
    // Declare that the native Objective-C class will implement the UIApplicationDelegate Objective-C protocol.
    protocols: [UIApplicationDelegate]
});

You can implement only some methods of the protocol. If a not implemented method is called an exception will be raised at runtime. You can implement only some or none of the optional methods.

Exposed Method Example

The following example shows how you can create a new instance method accessible from Objective-C APIs, that is declared in JavaScript:

var MyViewController = UIViewController.extend({
    viewDidLoad: function () {
        // ...
        var aboutButton = UIButton.buttonWithType(UIButtonType.UIButtonTypeRoundedRect);
        // Pass this target and the aboutTap selector for touch up callback.
        aboutButton.addTargetActionForControlEvents(this, "aboutTap", UIControlEvents.UIControlEventTouchUpInside);
        // ...
    },
    // The aboutTap is a JavaScript method that will be accessible from Objective-C.
    aboutTap: function(sender) {
        var alertWindow = new UIAlertView();
        alertWindow.title = "About";
        alertWindow.addButtonWithTitle("OK");
        alertWindow.show();
    },
}, {
    name: "MyViewController",
    exposedMethods: {
        // Declare the signature of the aboutTap. We can not infer it, since it is not inherited from base class or protocol.
        aboutTap: { returns: interop.types.void, params: [ UIControl ] }
    }
});

Overriding Initializers

Initializers should always return a reference to the object itself, and if it cannot be initialized, it should return null. This is why we need to check if self exists before trying to use it.

var MyObject = NSObject.extend({
    init: function() {
        var self = this.super.init();
        if (self) {
            // The base class initialized successfully
            console.log("Initialized");
        }
        return self;
    }
});

TypeScript Support

You can use TypeScript to inherit from native classes.

// A native class with the name "JSObject" will be registered, so it should be unique
class JSObject extends NSObject implements NSCoding {
    public encodeWithCoder(aCoder) { /* ... */ }

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

    public "selectorWithX:andY:"(x, y) { /* ... */ }

    // An array of protocols to be implemented by the native class
    public static ObjCProtocols = [ NSCoding ];

    // A selector will be exposed so it can be called from native.
    public static ObjCExposedMethods = {
        "selectorWithX:andY:": { returns: interop.types.void, params: [ interop.types.id, interop.types.id ] }
    };
}

There should be no TypeScript constructor, because it will not be executed. Instead override one of the init methods.

IMPORTANT NOTICE Currently this syntax is unsupported when using TypeScript with ES6 modules. As a workaround you can use the JavaScript approach by casting the base class to any and calling the extend function like this:

const AppDelegate = (UIResponder as any).extend({
    applicationDidBecomeActive(application: UIApplication): void {
        console.log("applicationDidBecomeActive", application);
    }
}, {
    protocols: [UIApplicationDelegate]
});

For updates regarding this issue you can check here

TypeScript Delegate Example

When working with native APIs, you'll find yourself having to setup delegates in order to recieve results or callbacks. For this example, we'll setup a delegate for the Tesseract-OCR-iOS API.

Let's first take a look at what the delegate typescript declarations look like:

interface G8TesseractDelegate extends NSObjectProtocol {
    preprocessedImageForTesseractSourceImage?(tesseract: G8Tesseract, sourceImage: UIImage): UIImage;
    progressImageRecognitionForTesseract?(tesseract: G8Tesseract): void;
    shouldCancelImageRecognitionForTesseract?(tesseract: G8Tesseract): boolean;
}

What we want to do is define a class G8TesseractDelegateImpl that extends NSObject and implements G8TesseractDelegate which looks like this:

class G8TesseractDelegateImpl
    extends NSObject // native delegates mostly always extend NSObject
    implements G8TesseractDelegate {

    static ObjCProtocols = [G8TesseractDelegate] // define our native protocalls

    static new(): G8TesseractDelegateImpl {
        return <G8TesseractDelegateImpl>super.new() // calls new() on the NSObject
    }

    preprocessedImageForTesseractSourceImage(tesseract: G8Tesseract, sourceImage: UIImage): UIImage {
        console.info('preprocessedImageForTesseractSourceImage')
        return sourceImage
    }

    progressImageRecognitionForTesseract(tesseract: G8Tesseract) {
        console.info('progressImageRecognitionForTesseract')
    }

    shouldCancelImageRecognitionForTesseract(tesseract: G8Tesseract): boolean {
        console.info('shouldCancelImageRecognitionForTesseract')
        return false
    }

}

Now that we have our delegate class setup, we can create a new instance of G8Tesseract and start using it:

function image2text(image: UIImage): string {
    let delegate: G8TesseractDelegateImpl = G8TesseractDelegateImpl.new()
    let tess: G8Tesseract = G8Tesseract.new()
    tess.delegate = delegate
    /*=============================
    =            NOTES            =
    =============================*/
    // The `tess.delegate` property is weak and won't be retained by the Objective-C runtime so you should manually keep the delegate JS object alive as long the tessaract instance is alive
    /*=====  End of NOTES  ======*/
    tess.image = image
    let results: boolean = tess.recognize()
    if (results == true) {
        return tess.recognizedText
    } else {
        return 'ERROR'
    }
}

Limitations

  • You shouldn't extend an already extended class
  • You can't override static methods or properties
  • You can't expose static methods or properties