Creating [object] in a different context than the calling function.

Creating [object] in a different context than the calling function.

This warning occured when a JS callback function is passed to Objective-C as KrollCallback and then executed on non-JS thread.

KrollCallback *callback = [args objectForKey@"callback"];

[OneSignal initWithLaunchOptions:[TiApp app].launchOptions appId:appId handleNotificationAction:^(OSNotificationResult *result) {
    [callback call:@[] thisObject:nil];
}];

Problem with this warning is, if the JS callback create Ti.Network.HTTPClient instance, then that instance is null.

function callback() {
    var http = Ti.Network.createHTTPClient();
    http.open('GET', getUrl()); // JS error! http is null
    http.send();
}

To fix, get krollContext instance of current module/proxy and execute callback inside invokeBlockOnThread

// For TiModule / TiProxy descendants
KrollContext *context = [self.executionContext krollContext];

// For TiUIView descendants
KrollContext *context = [self.proxy.executionContext krollContext];

// invoke
[context invokeBlockOnThread:^{
    [callback call:@[] thisObject:nil];
}];

Ti.Network.registerForPushNotifications no response

SDK: 3.5.1.GA, CLI: 4.0.1

Calling Ti.Network.registerForPushNotifications() has no response, whether success, error or callback, basically no response at all. Turned out that the build script can’t detect the method and add USE_TI_NETWORKREGISTERFORPUSHNOTIFICATIONS to defines.h during app compilation.

I created with a plugin that append the macro definition to *.pch file, here it is:

Create plugin folder:

cd $PROJECT_DIR
mkdir -p plugins/ti.pushnotifsymbol/hooks
touch plugins/ti.pushnotifsymbol/package.json
touch plugins/ti.pushnotifsymbol/hooks/pushnotifsymbol.js

pushnotifsymbol.js:

exports.id = 'ti.pushnotifsymbol';
exports.cliVersion = '>=3.2';
exports.init = function (logger, config, cli) {
    var path = require('path');
    var fs = require('fs');
    var util = require('util');
    var os = require('os');
    var pchFile;
    var triggered = false;
    cli.on('build.pre.compile', function(builder, next) {
        pchFile = path.join(builder.buildDir, builder.tiapp.name + '_Prefix.pch');
        next();
    });
    cli.on('build.ios.copyResource', function(builder, next) {
        if (!triggered) {
            triggered = true;
            if (fs.existsSync(pchFile)) {
                fs.appendFileSync(pchFile, os.EOL + '#define USE_TI_NETWORKREGISTERFORPUSHNOTIFICATIONS');
            }
        }
        next();
    });
};

Add <plugin>ti.pushnotifsymbol</plugin> to <plugins> section of your project tiapp.xml

Titanium iOS builder script

Location: TITANIUM_SDK/mobilesdk/osx/3.5.1.GA/iphone/cli/commands/_build.js
(TITANIUM_SDK is commonly at ~/Library/Application Support/Titanium)

1. Cannot change CFBundleVersion

  • CFBundleVersion is the build version number, available to developer and beta testers.
  • CFBundleShortVersionString is the release version number, what the users see on App Store
  • CFBundleVersion is automatically assign to value of <version> in tiapp.xml, but for beta testing purposes, need to change this value everytime we upload new build to iTunes Connect TestFlight
  • To fix this, modify iOS builder script in iOSBuilder.prototype.createInfoPlist() function, around line 2063
plist.CFBundleVersion = String(this.tiapp['build-version'] || this.tiapp.version);

2. If your Titanium SDK location is different from default (custom SDK path), the script doesn’t look up that folder when searching for modules

  • My Titanium SDK is located at ~/Applications/opt/Titanium, when running the ti build -p ios command, it doesn’t lookup for modules there
  • If my project included ti.cloud module, the build script will fail, cannot find the specified module
  • To fix this, in iOSBuilder.prototype.validate() function
// around line 1321, add:
customSDKPaths = config.get('paths.sdks'),

// around line 1331, add:
Array.isArray(customSDKPaths) && customSDKPaths.forEach(addSearchPath);

Android BOOT_COMPLETED handler module

This Titanium Android module provide a place where you can run your JS script when the device receives BOOT_COMPLETED broadcast intent. By simply adding boot_completed.js into your Resources folder, this JS file will be run when the broadcast intent received.

Continue reading Android BOOT_COMPLETED handler module

Android Progress Notification module

Android Progress Notification module is a module to display notification in Android notification area which contains progress bar. In addition, it also support setting the number of notification, useful if you have a number of grouped notifications.

nc_progress_notif_1 nc_progress_notif_2

Continue reading Android Progress Notification module

Titanium app hex color value with alpha channel

Color values are represented in hexadecimal value:

#000000

  • 0 – red, 0 – green, 0 – blue
  • color = black

#ffffff

  • 255 – red, 255 – green, 255 – blue
  • color = white

‘ff’ in hexadecimal is 255 in decimal, (2 ^ 8) – 1 = 255.

In CSS, to represent color with alpha channel, we can use the rgba() syntax:

rgba(0, 0, 0, 0.6)

  • 0 – red, 0 – green, 0 – blue
  • black with 60% opacity

In Titanium, rgba() syntax only available on iOS, but hex value also can be used to represent alpha channel, and it supported by both iOS & Android

#ff000000

  • 0 – red, 0 – green, 0 – blue
  • ff – 255 / 100% opacity

#90ffcc00

  • 255 – red, 204 – green, 0 – blue
  • 90 – 144 / 56% opacity

To easily maintain colors in app, this function is to transform rgba value into Titanium hex color format

function rgbaToHex(r, g, b, a) {
    var toHex = function(n) {
        return ('00' + (n | 0).toString(16)).slice(-2);
    };
    return '#' + toHex(((a * 100) / 100) * 255) + toHex(r) + toHex(g) + toHex (b);
}

Explanation:

  • (n | 0) is shortcut for parseInt(), to ensure the value passed in is integer. We don’t want the hex color value to have decimal point, e.g: #2.4ccc
  • .toString(16) is Number object function (not Object.toString()) to convert number into hexadecimal format
  • ('00' + value).slice(-2) is to add string padding to the left of the string, so that it will always have 2 characters. We don’t want the value to have one character, #0ccff is invalid value

Usage:

var view = Ti.UI.createView({
    backgroundColor: rgbaToHex(255, 204, 0, 0.5)
});

Lazyloading JS modules in Titanium app

In NodeJS project, usually modules are loaded at the beginning of the file:

var fs = require('fs');
var path = require('path');
var express = require('express');

When this NodeJS application being run, it loads all of the dependencies & start executing the program. In large application, there will be delay before the application can start operating, because of the dependencies loading.

For desktop or server application, this won’t be much issue, but in mobile app like Titanium, if it loads the whole dependencies during app launch, it might cause unresponsive app & consume large memory, even when those modules haven’t being used yet.

Another problem is there might be circular dependencies issue, in which module A require module B, but module B also require module A, and this lead to module A variable in B to become an empty object. Example:

// db.js - database module
var Person = require('model/Person');
Person.setup();

// Person.js - data model
var db = require('db');
db.execute('sql'); // TypeError, undefined is not a function

Restructuring the code flow is one method to fix, but in my experience, it cause a lot of repeated code

// Person.js
var Person = {};
Person.setup = function() {
     var db = require('db');
};

Person.getById = function() {
     var db = require('db');
};

Person.getAll = function() {
     var db = require('db');
};

Because of this, I use the lazyloading approach & make use of JS object getter to both solve the problem & make the code cleaner. The idea is to have a global variable, which will be included in each of the modules we have in a Titanium app, and this global object holds a reference to every modules we packaged into the app. This reference will then call a getter to do require() and return the actual module to the caller.

In addition, modules are categorized into folders which can act as namespace, to eliminate class name conflict

// globals.js
var g = {};

var modules = [
     'window/MainWindow',
     'ui/ContactList',
     'ui/ContactListItem',
     'model/Contact',
     'core/Db',
     'core/Http'
];

modules.forEach(function(mod) {
     var parts = mod.split('/');
     var namespace = parts[0];
     var className = parts[1];
     var obj = g[namespace];
     if (!obj) {
          obj = {};
     }
     Object.defineProperty(obj, className, {
          get: (function(path) {
               return function() {
                    return require(path);
               };
          })(mod),
          set: function() {}
     });
});

module.exports = g;

Now, the Person model class can be refactored like below:

// Person.js
var g = require('globals');
var Person = {};
Person.setup = function() {
     g.core.Db.execute('sql');
};
Person.getAll = function() {
     g.core.Db.execute('sql');
};

This is just a basic implementation of the idea, and you can extend it to support subnamespace & native module.

Add custom framework to Titanium iOS module

For example, I’m creating a module com.mymod, which include MillennialMedia SDK, that has 2 custom frameworks:

  • MillennialMedia.framework
  • SpeechKit.framework

Copy the frameworks into <module folder>/platform/iphone/custom_frameworks

Open the module in Xcode & include the framework – you can choose to include into any folder, for example the root of project

Edit module.xcconfig. Choose a unique variable name to set the environment variables (e.g MYMOD)

MYMOD_ID=com.mymod
MYMOD_VER=1.0
MYMOD_DIR=$(SRCROOT)/../../modules/iphone/$(COM_MYMOD_MYMOD_ID)/$(COM_MYMOD_MYMOD_VER)/platform/iphone

OTHER_LDFLAGS=$(inherited) -F"$(COM_MYMOD_MYMOD_DIR)/custom_frameworks" -framework MillennialMedia -F SpeechKit

Notice that we’re not using the defined env vars. This is because Titanium build script rename the var based on the module id (com.mymod becomes COM_MYMOD). If we’re referring to the var name that we define, it produces distorted value, example, MYMOD_VER become ” 1.0 1.0″

Reference: https://developer.appcelerator.com/question/132459/module-dev-third-party-framework

ScrollableView in ListView

Using ListView is all about mapping collection data structure to UI structure. What if we have a UI structure that have nested collection in a single ListItem, such as a ScrollableView? We have to make sure our data structure can map to the ScrollableView.views property.

News (data) Ti.UI
previews[] ScrollableView.views
title Label.text
timestamp Label.text

The solution: we have to create each child view when setting items to ListView. Here’s how:

var win = Ti.UI.createWindow({
    exitOnClose: true,
    title: 'ScrollableView in ListView'
});

var newsTemplate = {
    childTemplates: [
        {
            type: 'Ti.UI.View',
            properties: {
                width: 300,
                backgroundColor: '#fff',
                borderColor: '#ccc',
                layout: 'vertical',
                top: 10
            },
            childTemplates: [
                {
                    type: 'Ti.UI.ScrollableView',
                    bindId: 'previews',
                    properties: {
                        width: 300,
                        height: 150
                    }
                },
                {
                    type: 'Ti.UI.Label',
                    bindId: 'title',
                    properties: {
                        color: '#333',
                        width: Ti.UI.FILL,
                        textAlign: Ti.UI.TEXT_ALIGNMENT_LEFT,
                        left: 5,
                        right: 5,
                        wordWrap: false,
                        ellipsize: true,
                        font: {
                            fontSize: '14dp',
                            fontWeight: 'bold'
                        }
                    }
                },
                {
                    type: 'Ti.UI.Label',
                    bindId: 'published',
                    properties: {
                        color: '#aaa',
                        width: Ti.UI.FILL,
                        textAlign: Ti.UI.TEXT_ALIGNMENT_LEFT,
                        left: 5,
                        right: 5,
                        wordWrap: false,
                        ellipsize: true,
                        font: {
                            fontSize: '12dp'
                        }
                    }
                }
            ]
        }
    ]
};

var sect = Ti.UI.createListSection();

var listView = Ti.UI.createListView({
    templates: {
        news: newsTemplate,
        foot: {
            properties: {
                height: 10
            }
        }
    },
    backgroundColor: '#eee',
    separatorColor: '#eee',
    sections: [sect]
});
win.add(listView);

var data = [
    {
        title: 'News A',
        timestamp: 1411789472624,
        previews: [
            'http://placehold.it/300x150/0099cc/ffffff',
            'http://placehold.it/300x150/9933cc/ffffff',
            'http://placehold.it/300x150/669900/ffffff',
            'http://placehold.it/300x150/ff8800/ffffff'
        ]
    },
    {
        title: 'News B',
        timestamp: 1411443872624,
        previews: [
            'http://placehold.it/300x150/cc0000/ffffff',
            'http://placehold.it/300x150/33b5e5/ffffff',
            'http://placehold.it/300x150/aa66cc/ffffff'
        ]
    },
    {
        title: 'News C',
        timestamp: 1411184672624,
        previews: [
            'http://placehold.it/300x150/99cc00/ffffff',
            'http://placehold.it/300x150/ffbb33/ffffff',
            'http://placehold.it/300x150/ff4444/ffffff',
            'http://placehold.it/300x150/0099cc/ffffff',
            'http://placehold.it/300x150/9933cc/ffffff',
        ]
    }
];

win.addEventListener('open', function() {
    var items = [];

    var getDate = function(timestamp) {
        var d = new Date(timestamp);
        var m = d.getMonth() + 1;
        if (m > 12) {
            m = 1;
        }
        return d.getDate() +'/'+ m +'/'+ d.getFullYear();
    };

    for (var i = 0; i < data.length; i++) {
        var d = data[i];

        var views = [];
        for (var j = 0; j < d.previews.length; j++) {
            views.push(Ti.UI.createImageView({
                image: d.previews[j],
                width: 300,
                height: 150
            }));
        }

        items.push({
            template: 'news',
            previews: {
                views: views
            },
            title: {
                text: d.title
            },
            published: {
                text: 'Published on '+ getDate(d.timestamp)
            }
        });
    }

    items.push({
        template: 'foot'
    });

    sect.setItems(items);
});

win.open();

Output:
scrollableview-in-listview