明壁幕府忍法帳 > Aptana Index > Home > Titanium SDK > Titanium SDK How-tos > Extending Titanium Mobile > iOS Module Development Guide > iOS Module Architecture

2017.07.25 Ver.18 (2018.12.28)

iOS Module Architecture

Introduction

The Titanium SDK is based on a modular architecture, which can be utilized to extend the SDK by building modules.  The module architecture contains the following key interface components:

  • Proxy: A base class that represents the native binding between your JavaScript code and native code
  • ModuleA special type of Proxy that describes a specific API set or namespace
  • ViewProxyA specialized Proxy that knows how to render Views
  • ViewThe visual representation of a UI component which Titanium can render

When building a Module, you can only have one Module class but you can have zero or more Proxies, Views and ViewProxies.

To return visual data between JavaScript and native code, create a View and a ViewProxy.  Each View requires a View Proxy.  The ViewProxy represents the model data (which is kept inside the proxy itself in case the View needs to be released) and is responsible for exposing the APIs and events that the View supports.

To return non-visual data between JavaScript and native code, create a Proxy. The Proxy knows how to handle any method and property, and dispatching and firing events.

These components require special class names and method signatures.  Each components is described in further detail below.

Example module

Check out the Ti.ModDevGuide module on Github that uses the following best practices to build a cross-platform Titanium module for iOS / Android!

Module

A Module is a class that provides an API point with a particular ID. That ID can then be used to require the module from JavaScript.  Each module must have one module and only one module class can be defined.

Module's can also expose properties and methods to JavaScript.  See the Proxy section for more information.

Module Class

To create a Module, the class name and filename is the module ID in camel case notation and suffixed with Module. The class must extend the TiModule class.  Note that when you create a new module, you already have a boilerplate module class.

ComExampleTestModule.h
#import "TiModule.h"
@interface ComExampleTestModule : TiModule
@end

To access the module from JavaScript, pass the module ID to the require method.

var module = require('com.example.test');

Module Lifecycle

The module provides several places for you to hook into the application's lifecycle.

When you create a iOS module, the boiler plate code provides you with the default startup and shutdown methods to execute code when the module is loaded or unloaded from the application, respectively.

You can also use the load method to execute code before the application starts.  This allows the application to register callbacks for lifespan events, such as application:didFinishLaunchingWithOptions.

For items you want to inject right into the runtime, use the performSelectorDuringRunLoopStart method to assign a callback that executes every time a run loop starts before the file is executed.  The selector is a class method that takes one argument, which is the TiBindingRunLoop method.  This is useful for modifying the runtime or for debugging by capturing exceptions before the application has been parsed.

+ (void)doALittleDance:(TiBindingRunLoop)runLoop
{
    NSLog(@"I'm doing a little dance for %@", runLoop);
}
// Binds the previous method before the application starts.
// Function executes when the runtime loop starts.
+ (void)load
{
    NSLog(@"I'mma do a little dance.");
    [self performSelectorDuringRunLoopStart:@selector(doALittleDance:)];
}

Proxy

A Proxy can expose methods and properties to JavaScript. Each of these can be an NSObject, nil or a proxy.  Proxies also have built-in event management

Proxy Class

To declare a Proxy, the class name and filename must be prefixed with the module ID in camel case notation and suffixed with Proxy. The class must extend the TiProxy class.

ComExampleTestFooProxy.h
#import "TiProxy.h"
@interface ComExampleTestFooProxy : TiProxy
@end
ComExampleTestFooProxy.m
#import "ComExampleTestFooProxy.h"
@implementation ComExampleTestFooProxy
@end

When you declare a Proxy in this manner, it exposes a create method to JavaScript to allow you to create an instance of the Proxy using the module reference.  The create method is the name of the class without the module ID prefix and Proxy suffix, with a create prefix.  For example, the previous class will have a createFoo() method.

var module = require('com.example.test');
var fooProxy = module.createFoo();

 

Proxy Methods

Proxies expose methods and properties by simply using standard Objective-C syntax. To expose a method, a Proxy must have one of the following valid signatures:

// This signature is used when the method returns a value to the caller.
// The returned result can be either a valid NSObject, nil or a Proxy (or subclass thereof).
- (id)methodName:(id)args
{
    // Call the method with "var result = foo.methodName()"
}
// This signature should be used if your method returns no value.
- (void)methodName:(id)args
{
    // Call your method with "foo.methodName(args)"
}

 

Both signatures take a single argument (args) as a NSArray. However, generally, we recommend using the id declaration. This will make it easier to typecast the incoming value to another value, which we'll describe below.

If you function takes no arguments, you can simply ignore the incoming args value.

Titanium provides several convenience macros for typecasting incoming values to a specific type and extracting them from the array.

ENSURE_SINGLE_ITEM(args,type)
  • The first parameter is the name of the argument.
  • The second parameter is the type name that the value should be.

This macro will do two actions:

  1. Pull out the first argument, that is, [args objectAtIndex:0].
  2. Cast the return value to the type passed in the second argument.

This macro can only be used for single-argument methods. If you have multiple arguments, you should simply used the normal array accessor methods.

ENSURE_UI_THREAD_0_ARGS

This macro can be used to ensure that the current method only runs on the UI thread (main thread). If the method is invoked on a non-main thread, it will simply re-queue the method on the UI thread. This method is equivalent to [NSThread performSelectorOnMainThread].

You can only use this method if you have no return result.

ENSURE_UI_THREAD_1_ARG(arg)

This macro can be used to ensure that the current method, with argument, only runs on the UI Thread. It is the same as the previous macro with the exception that it will ensure that the arguments are passed along, too.

Titanium provides a Utility library for converting and checking certain values. This library is in TiUtils and can be imported with:

#import "TiUtils.h"

Some common conversion utility examples:

CGFloat f = [TiUtils floatValue:arg];
NSInteger f = [TiUtils intValue:arg];
NSString *value = [TiUtils stringValue:arg];
NSString *value = [TiUtils stringValue:@"key" properties:dict def:@"default"];
TiColor *bgcolor = [TiUtils colorValue:arg];

Proxy Properties

To expose a JavaScript property in a Proxy, you can define a property using the Objective-C @property notation with the copy attribute:

SomeProxy.h
@interface SomeProxy: TiUIView {
 
}
 
@property(copy) NSString* propertyName;
 
@end

 

To expose a property in a Proxy with a custom setter or getter, declare the variable in the class's header file and implement the setter and getter methods:

SomeProxy.h
@interface SomeProxy: TiUIView {
    NSString *foo;
}
@end
SomeProxy.m
- (void)setPropertyName:(id)value
{
    ENSURE_STRING(value);
    foo = [value retain];
}
 
- (NSString *)propertyName
{
    return foo;
}

In the setter method, the method name must be the property name with the first letter capitalized and prefixed with set. Titanium will pass a single value as the converted value as the first argument (instead of an NSArray like a method).

In the getter method, your property method must return a value as either a NSObject, nil or TiProxy (or subclass).

Type Conversions

Returning Object Values

The following Objective-C types can be returned without conversion:

Objective-C

NSString

NSDictionary

NSArray

NSNumber NSDate NSNull
JavaScript

String

Object

Array

Number Date null

Returning Primitive Values

To return a primitive value from either a method or property, you should return an NSNumber with the appropriate wrapped primitive value. Titanium provides a set of macros to make this easier:

Macro

Description

NUMINT

Equivalent to [NSNumber numberWithInt:value]

NUMBOOL

Equivalent to [NSNumber numberWithInt:value]

NUMLONG

Equivalent to [NSNumber numberWithLong:value]

NUMLONGLONG

Equivalent to [NSNumber numberWithLongLong:value]

NUMDOUBLE

Equivalent to [NSNumber numberWithDouble:value]

NUMFLOAT

Equivalent to [NSNumber numberWithFloat:value]

Returning Complex Values

There are two approaches to returning complex values:

  • The first approach is to set values in a NSDictionary and it. When returning a NSDictionary, Titanium converts this to a JavaScript object with each key/value being mapped into JavaScript object property/values.
  • The second approach is to create a specialized Proxy. The Proxy should then be returned which will be exposed as a JavaScript object with functions and properties. Invocation against the returned Proxy will be invoked against your returned proxy instance. When you return a Proxy instance, you must autorelease it if you created it in your method.

Returning Files

To return a reference to a filesystem file, you should return an instance of a TiFile proxy. This will automatically handle exposing the native file and it's methods and properties. To return a file, you can use the following example:

return [[[TiFile alloc] initWithPath:path] autorelease];

Returning Blobs

To return a reference to a blob data (such as a NSData), you should return an instance of a TiBlob. This will automatically handle exposing some native blob operations. To return a blob, you can use the following examples:

return [[[TiBlob alloc] initWithData:nsdata mimetype:@"application/octet-stream"] autorelease];
return [[[TiBlob alloc] initWithImage:image] autorelease];
return [[[TiBlob alloc] initWithFile:file] autorelease];

In the first example above, the second argument should map to the mime type of the raw data content. If the data is binary, you can use the "application/octet-stream" value.

Returning CGRect

To return a CGRect, Titanium provides a proxy named TiRect. You can use the following example:

TiRect *result = [[[TiRect alloc] init] autorelease];
[result setRect:rect];
return result;

Returning CGPoint

To return a CGPoint, Titanium provides a proxy named TiRect. You can use the following example:

return [[[TiPoint alloc] initWithPoint:point] autorelease];

Returning NSRange

To return an NSRange, create an NSDictionary using the NSRange values and return the NSDictionary.
TiRange was used to return an NSRange in the past. TiRange has been removed from the SDK as of version 2.1.0 in favor of NSDictionary.
You can use the following example:

return [NSDictionary dictionaryWithObjectsAndKeys:NUMINT(range.location),@"location",NUMINT(range.length),@"length", nil];

Returning UIColor

To return a UIColor, Titanium provides a proxy named TiColor. You can use the following example:

return [[[TiColor alloc] initWithColor:color name:@"#fff"] autorelease];

The second argument (name) should be the textual name (if provided) of the UIColor.

Setting Proxy Values

When you create a proxy from JavaScript, you typically pass an optional dictionary of key/value pairs. By using this pattern, Titanium provides built-in functionality to help make proxy programming easier.

Let's use a simple example:

var my_module = require('com.example.test');
var foo = my_module.createFoo({
  'bar': '123'
});

Titanium will automatically correctly define and dispatch proxy factory creation methods like above. You simply need to define a Proxy with the same name as your module and the name of your method plus the Proxy suffix.

ComExampleTestFooProxy.h
ComExampleTestFooProxy.m

Your module does not need to define the createFoo method. Using this convention, Titanium already knows how to do that for you.

In your proxy, you would then want to define a setter method to handle the bar property. Upon construction (the init), Titanium will automatically call the setter for all properties passed in the constructor.

#import "ComExampleTestFooProxy.h"#import "TiUtils.h"
@implementation ComExampleTestFooProxy
- (void)setBar:(id)value
{
   NSString *str = [TiUtils stringValue:value];
}
@end

In the above example, we simply define the setter and use the TiUtils to convert the value into a stringValue. Using this utility ensures that no matter what type of value passed into the argument, we'll get the string representation. So, if the user was to pass the number 123, it would still return an NSString with the value @"123".

Proxies are also special about how they handle and hold their properties. Proxies will always store values passed in a special internal NSDictionary called 'dynprops'. This means that you can always use the following method to retrieve the value of the proxy without having to define getters and setters for each of your properties:

id value = [self valueForUndefinedKey:@"bar"];

If you use valueForUndefinedKey, you will always retrieve the original property value. However, if you want to invoke a potential getter (which may or may not return the original property value in JavaScript), you should use the following:

id value = [self valueForKey:@"bar"];

In the above code example, if we had defined a method like the following below it would be invoked instead of retrieving our internal original property.

- (id) bar
{
  return @"456";
}

Properties don't have to be passed in the constructor for them to be internally set and your setter invoked.

foo.bar = 789;

When you invoke the property of a proxy, the following will happen:

  • If you have defined a setter, it will be invoked.
  • If you have not defined a setter, the property and value will be stored internally in the dynprops.

If you implement a setter, you should also manually store the property yourself in dynprops. You can do this by calling the following method:

- (void)setBar:(id)value
{
  [self replaceValue:value forKey:@"bar" notification:NO];
}

The third argument (notification) tells Titanium whether you want the setter to be invoked from this property change. Since we're already inside our setter, we don't want an infinite recursion so we pass NO.

Handling Events

Proxies automatically handle firing events and managing event listeners. Internally, when you call addEventListener() or removeEventListener() from JavaScript against a proxy instance, the proxy will automatically handle the code for managing the event listeners.

If you want to be notified upon an add or remove, you should override the methods:

- (void)_listenerAdded:(NSString*)type count:(int)count
{
    // A new listener was added
}
 
- (void)_listenerRemoved:(NSString*)type count:(int)count
{
    // An existing listener was removed
}

The _listenerAdded method will be invoked with the event name (type) and the total number of existing listener with the same type (including the new event listener). This is convenient, for example, when you would like to enable some action only once at least one listener is listening for the event. This can be useful for conserving system resources.

The _listenerRemoved method will be invoked with the event name (type) and the total number of remaining listeners with the same type (excluding the removed listener). This is useful when you want to cleanup system resources once no listeners are actively listening for events. To send an event to any event listener, you use the convenience method:

- (void)fireEvent:(NSString *)type withObject:(id)obj;

The first argument is the default and most common way to fire an event. The first argument (type) is the event name. The second argument (obj) is an NSDictionary of event properties. The second argument can also be nil if no additional event properties are needed. The event properties will be part of the event argument which is the first argument in all event functions.

For example:

- (void)myMethod
{
    [self fireEvent:@"foo" withObject:@{@"name": @"foo"}];
}

In this example, we'd adding one additional event property named 'name' with the value of 'foo'. In JavaScript, this would be retrieved like:

foo.addEventListener('foo', function(e) {
  alert('Name is ' + e.name);
});

In addition to any additional event arguments passed, Titanium automatically provides the following built-in properties on all events fired:

  • source — The source object (proxy) that fired the event
  • type — The type of event

You can also check for the existing of a listener (recommended) before firing an event. This is done with the _hasListeners method.

- (void)fireMyEvent
{
    if ([self _hasListeners:@"foo"]) {
        // Fire event
        [self fireEvent:@"foo" withObject:@{@"name": @"foo"}];
    }
}

It is generally recommended that you only construct your event object and fire the event if you have listeners to conserve processing power.

Memory Management

Proxies act like any Objective-C class and all memory management rules must be considered. When returning a new proxy instance from a method, you must autorelease the instance. Titanium will retain a reference to the proxy which maps to a reference to the resulting JavaScript variable reference. However, once the JavaScript variable is no longer referenceable, it will be released and your proxy will be sent the dealloc message.

You must take special care to retain/release your objects in Titanium just like you would in any Objective-C based programs. Improper retain/release will cause crashes and undesired results. Modules created with Titanium SDK 6.1.0 or later will use ARC (Automatic Reference Counting) be default, so you do not need to retain/release objects manually anymore. But please remember that there might still be use-cases where you need to use memory-concepts like the autorelease-pool to release objects properly (e.g. when manipulating image-references inside a loop). See this guide on how to migrate a non-ARC code-base to ARC!

View Proxy and View

To display UI elements with a module, create both a View Proxy and a View.  A View Proxy is a special Proxy that is used for Views — objects which interact with the UI to draw things on the screen.

The View Proxy holds the data (model) and acts like a controller for dispatching property changes and methods against the view. The View handles the internal logic for interacting with UIKit and handling UIKit events. The View is a model delegate to the View Proxy, and as long as referenced, receives property changes.

The View Proxy does not always retain a valid reference to a View. The View is only created on demand, as needed. Once the View is created and retained by the View Proxy, all property change events on the View Proxy will be forwarded to the View.

To call the method to create the view from JavaScript, call the module's create*View() method.  The name of the method is the name of the view class without the module ID, then prefixed with create.  For example, if the class was called ComExampleTestFooView, the method would be called createFooView().

var module = require('com.example.test');
var fooView = module.createFooView(/* params */);
var win = Ti.UI.createWindow();
win.add(fooView);
win.open();

For Alloy projects, you can also use the Module element.  Set the module attribute to the module ID and set the method attribute to a method that returns a View.  You can optionally pass in initialization parameters for the create method.

<Alloy>
    <View>
        <Module module="com.example.test" method="createFooView"/>
    </View>
</Alloy>

View Proxy Class

To declare a View Proxy, the class name and filename must be prefixed with the module ID in camel case notation and suffixed with ViewProxy. The class must extend the TiViewProxy class.

ComExampleTestFooViewProxy.h
#import "TiViewProxy.h"
@interface ComExampleTestFooViewProxy: TiViewProxy {
    // Your private properties go here
}
@end 
ComExampleTestMyViewProxy.m
#import "ComExampleTestFooViewProxy.h"
@implementation ComExampleTestFooViewProxy
@end

 

To get a reference to the View, call the View Proxy's view method: 

UIView *view = [self view];

View Class

To declare a View Proxy, the class name and filename must be prefixed with the module ID in camel case notation and suffixed with View. The class must extend the TiUIView class.  The TiUIView class extends UIView and provides Titanium specific functionality.

ComExampleTestFooView.h
#import "TiUIView.h"
@interface ComExampleTestFooView : TiUIView {
 
 
}@end
ComExampleTestFooView.m
#import "ComExampleTestFooView.h"
@implementation ComExampleTestFooView
@end

The View will be attached to the UIView hierarchy as needed by the View Proxy. However, if you have sub-views you will need to attach them to yourself (TiUIView) as necessary. This is typically done by assigning your view to an instance variable and keeping a reference to it and only creating and attaching when the reference is nil.

View Properties

Properties that can be set on views can either be defined in the View class or the ViewProxy class.

The view property setters are named using a special required convention.

To define a property in a View, capitalize the first character of the property, then prefix it with set and suffix it with an underscore (_). 

ComExampleTestView.m
- (void)setFoo_:(id)value
{
    // Handle the value
}

To define a property in a View Proxy, capitalize the first character of the property, then prefix it with set. Do NOT add an underscore to the end.

ComExampleTestViewProxy.m
- (void)setFoo:(id)value
{
    // Handle the value
}

When you have properties in your View Proxy that should be handled by an active View, you don't need to define them in your View. Instead, you can define them using the syntax above in your View and they be automatically dispatched. In the event that your View is attached after properties have been set, the View Proxy will automatically forward all property change events (results in calling each setter method) to the View upon construction.

Note: to access the View from the View Proxy, use the View Proxy's view method.

UIView *view = [self view];

View Methods

The View Proxy must declare and dispatch any methods that it wants the View to also handle. For example:

- (void)show:(id)args
{
    [[self view] performSelectorOnMainThread:@selector(show:)
                                  withObject:args waitUntilDone:NO];
}

You can also use a convenience macro that does the equivalent:

USE_VIEW_FOR_UI_METHOD(methodName)

The following code example is the same as the show method above:

USE_VIEW_FOR_UI_METHOD(show)
  • ラベルなし