Titanium JIRA Archive
Titanium SDK/CLI (TIMOB)

[TIMOB-25478] Hyperloop: iOS - Cannot use Hyperloop.defineClass with a Classic (Non-Alloy) project

GitHub Issuen/a
TypeBug
PriorityCritical
StatusResolved
ResolutionWon't Do
Resolution Date2018-08-22T06:29:12.000+0000
Affected Version/sn/a
Fix Version/sn/a
ComponentsHyperloop, iOS
Labelsalloy, hyperloop, ios
ReporterJoe Falcone
AssigneeJan Vennemann
Created2017-11-03T14:46:25.000+0000
Updated2018-08-27T09:23:46.000+0000

Description

While the Hyperloop requirements state "Alloy 1.8.0+" does this mean that you MUST use Alloy for your UI or just that you have Alloy 1.8.0+ installed. We have tried to use Hyperloop with a Classic project for iOS and while the correct JS and Objective-C code was generated, the app was not able to resolve references to classes generated by the Hyperloop invocations. We put the same code into the Hyperloop-examples app (which is Alloy) and it compiled and ran OK. We can get more specific about the problems and submit example code but I wanted initially to see whether the intention was to allow Hyperloop to work with Classic (Non-Alloy UI) apps.

Attachments

FileDateSize
test_hyperloop_classic.zip2017-11-03T15:28:55.000+00004911706

Comments

  1. Hans Knöchel 2017-11-03

    The requirements of Alloy 1.8+ are actually only for using Hyperloop with common-js tags (e.g. exposing native api's as view tags like <UILabel /> using own tag-classes). I am pretty sure it should work with classic Titanium but let me attach a project to verify. *EDIT*: It works, I attached an example project. I probably know you issue :-) You may created a classic project, included the module but not the plugin. Please check the tiapp.xml of the attached example and let me know, I'll keep the ticket open until then.
  2. Joe Falcone 2017-11-03

    That wasn't the problem. We're trying to use the iOS Bluetooth Library and we can get the same code to work when we include it in the Hyperloop-examples app but not in our own Classic app. We'll attach our example app but for a preview... This is the error we get at runtime from this line... var CentralManagerDelegate = Hyperloop.defineClass('CentralManagerDelegate', 'NSObject', ['CBCentralManagerDelegate']); [ERROR] :  Cannot find class with name: CentralManagerDelegate ... /BLETest.app/centralmanagerdelegate.js
  3. Joe Falcone 2017-11-03

    It is starting to look to me like its all about the file system hierarchy with Classic doing its own thing vs Alloy. I was able to get defineClass to work in a Classic project but I'm waiting for my colleague to compare what I've done to what he was trying to do.
  4. Joe Falcone 2017-11-03

    So I went back to my colleague's test program and it is also very simple but it generates these errors from this call (I will attach his ap): var CentralManagerDelegate = Hyperloop.defineClass('CentralManagerDelegate', 'NSObject', ['CBCentralManagerDelegate']); Cannot find class with name: CentralManagerDelegate [ERROR] : Script Error { [ERROR] : column = 181; [ERROR] : description = "Cannot find class with name: CentralManagerDelegate"; [ERROR] : line = 1; [ERROR] : message = "Cannot find class with name: CentralManagerDelegate"; [ERROR] : name = ClassNotFound; [ERROR] : nativeStack = "1 libobjc.A.dylib 0x0000000111dc7f41 objc_exception_throw + 48\n2 BLETest 0x000000010af508e2 -[HyperloopClass initWithClassName:alloc:init:args:] + 386\n3 BLETest 0x000000010af43614 DefineClass + 452\n4 BLETest 0x000000010ad07c21 _ZN2TI19APICallbackFunction4callINS_18JSCallbackFunctionEEExPNS_9ExecStateE + 593\n5 BLETest 0x000000010adb6187 _ZN2TI5LLInt9setUpCallEPNS_9ExecStateEPNS_11InstructionENS_22CodeSpecializationKindENS_7TiValueEPNS_17LLIntCallLinkInfoE + 503\n6 BLETest 0x000000010adbabbd llint_op_call + 148"; [ERROR] : sourceURL = "file:///Users/jrf/Library/Developer/CoreSimulator/Devices/88EBE46A-7202-4E84-8CF2-D18FEA477C6A/data/Containers/Bundle/Application/CAD70CE8-BD5C-47D3-880E-FF5DAC2ADC7B/BLETest.app/centralmanagerdelegate.js"; [ERROR] : stack = "defineClass@[native code]\nfile:///Users/jrf/Library/Developer/CoreSimulator/Devices/88EBE46A-7202-4E84-8CF2-D18FEA477C6A/data/Containers/Bundle/Application/CAD70CE8-BD5C-47D3-880E-FF5DAC2ADC7B/BLETest.app/centralmanagerdelegate.js:1:181\nglobal code@file:///Users/jrf/Library/Developer/CoreSimulator/Devices/88EBE46A-7202-4E84-8CF2-D18FEA477C6A/data/Containers/Bundle/Application/CAD70CE8-BD5C-47D3-880E-FF5DAC2ADC7B/BLETest.app/centralmanagerdelegate.js:63:70\nrequire@[native code]\nfile:///Users/jrf/Library/Developer/CoreSimulator/Devices/88EBE46A-7202-4E84-8CF2-D18FEA477C6A/data/Containers/Bundle/Application/CAD70CE8-BD5C-47D3-880E-FF5DAC2ADC7B/BLETest.app/centralble.js:13:37\nglobal code@file:///Users/jrf/Library/Developer/CoreSimulator/Devices/88EBE46A-7202-4E84-8CF2-D18FEA477C6A/data/Containers/Bundle/Application/CAD70CE8-BD5C-47D3-880E-FF5DAC2ADC7B/BLETest.app/centralble.js:96:70\nrequire@[native code]\nfile:///Users/jrf/Library/Developer/CoreSimulator/Devices/88EBE46A-7202-4E84-8CF2-D18FEA477C6A/data/Containers/Bundle/Application/CAD70CE8-BD5C-47D3-880E-FF5DAC2ADC7B/BLETest.app/app.js:62:27\nglobal code@file:///Users/jrf/Library/Developer/CoreSimulator/Devices/88EBE46A-7202-4E84-8CF2-D18FEA477C6A/data/Containers/Bundle/Application/CAD70CE8-BD5C-47D3-880E-FF5DAC2ADC7B/BLETest.app/app.js:65:3"; [ERROR] : } [ERROR] : Script Error Module "centralmanagerdelegate.js" failed to leave a valid exports object [ERROR] : ErrorController is up. ABORTING showing of modal controller [ERROR] : Script Error Module "centralble.js" failed to leave a valid exports object [ERROR] : ErrorController is up. ABORTING showing of modal controller
  5. Joe Falcone 2017-11-03

    Maybe I'm missing something somewhere but I can't find where to add an attachment. In any case, here is the class definition file - centralmanagerdelegate.js:
       /**
        * @author Kon Lovett
        */
       
       var CentralManagerDelegate = Hyperloop.defineClass('CentralManagerDelegate', 'NSObject', ['CBCentralManagerDelegate']);
       
       CentralManagerDelegate.addMethod({
           selector : 'centralManagerDidUpdateState:',
           instance : true,
           arguments : ['CBCentralManager'],
           callback : function(central) {
               if (this.didUpdateState) {
                   return this.didUpdateState(central);
               }
           }
       });
       
       CentralManagerDelegate.addMethod({
           selector : 'centralManager:willRestoreState:',
           instance : true,
           arguments : ['CBCentralManager', 'NSDictionary'],
           callback : function(central, dict) {
               if (this.willRestoreState) {
                   return this.willRestoreState(central, dict);
               }
           }
       });
       
       CentralManagerDelegate.addMethod({
           selector : 'centralManager:didDiscoverPeripheral:advertisementData:RSSI:',
           instance : true,
           arguments : ['CBCentralManager', 'CBPeripheral', 'NSDictionary', 'NSNumber'],
           callback : function(central, peripheral, advertisementData, RSSI) {
               if (this.didDiscoverPeripheral) {
                   return this.didDiscoverPeripheral(central, peripheral, advertisementData, RSSI);
               }
           }
       });
       
       CentralManagerDelegate.addMethod({
           selector : 'centralManager:didConnectPeripheral:',
           instance : true,
           arguments : ['CBCentralManager', 'CBPeripheral'],
           callback : function(central, peripheral) {
               if (this.didConnectPeripheral) {
                   return this.didConnectPeripheral(central, peripheral);
               }
           }
       });
       
       CentralManagerDelegate.addMethod({
           selector : 'centralManager:didFailToConnectPeripheral:error:',
           instance : true,
           arguments : ['CBCentralManager', 'CBPeripheral', 'NSError'],
           callback : function(central, peripheral, error) {
               if (this.didFailToConnectPeripheral) {
                   return this.didFailToConnectPeripheral(central, peripheral, error);
               }
           }
       });
       
       CentralManagerDelegate.addMethod({
           selector : 'centralManager:didDisconnectPeripheral:error:',
           instance : true,
           arguments : ['CBCentralManager', 'CBPeripheral', 'NSError'],
           callback : function(central, peripheral, error) {
               if (this.didDisconnectPeripheral) {
                   return this.didDisconnectPeripheral(central, peripheral, error);
               }
           }
       });
       
       module.exports = CentralManagerDelegate;
       
  6. Joe Falcone 2017-11-03

    And here is the file that uses the new class... centralble.js
       /**
        * @author Kon Lovett
        */
       
       /* */
       
       var CBCentralManager = require('CoreBluetooth/CBCentralManager');
       var CBUUID = require('CoreBluetooth/CBUUID');
       
       var _ = require('underscore');
       
       //relative load
       var CentralManagerDelegate = require('centralmanagerdelegate');
       
       /*
       * Module Init
       */
       
       // CM delegate define
       var cenManDel = new CentralManagerDelegate();
       cenManDel.didUpdateState = didUpdateState;
       cenManDel.willRestoreState = willRestoreState;
       cenManDel.didDiscoverPeripheral = didDiscoverPeripheral;
       cenManDel.didConnectPeripheral = didConnectPeripheral;
       cenManDel.didFailToConnectPeripheral = didFailToConnectPeripheral;
       cenManDel.didDisconnectPeripheral = didDisconnectPeripheral;
       
       var centralManager = CBCentralManager.alloc().initWithDelegateQueueOptions(cenManDel, null, null);
       
       var kindsMatcher;
       
       /* */
       
       // kinds : [ string ]
       function scan(kinds) {
       
           Ti.API.info('Scanning Desired');
       
           // look for all or a subset of peripherals
           var nativeKinds = null;
           if (!(kinds || kindsMatcher) || !kindsMatcher(kinds)) {
               kindsMatcher = null;
               if (kinds) {
                   kindsMatcher = _.matcher(kinds);
                   nativeKinds = _.map(kinds, function(uuidStr) {
                       return CBUUID.UUIDWithString(uuidStr);
                   });
               }
           }
       
           centralManager.scanForPeripheralsWithServicesOptions(nativeKinds, null);
       
           Ti.API.info('Scanning Started');
       }
       
       /* Delegate Callbacks */
       
       function didUpdateState(central) {
           Ti.API.info('didUpdateState');
       };
       
       function willRestoreState(central, dict) {
           Ti.API.info('willRestoreState');
           Ti.API.info('dict: ' + JSON.strfingify(dict, null, 4));
       };
       
       function didDiscoverPeripheral(central, peripheral, advertisementData, RSSI) {
           Ti.API.info('didDiscoverPeripheral');
           Ti.API.info('peripheral: ' + JSON.stringify(peripheral, null, 4));
           Ti.API.info('advertisementData: ' + JSON.stringify(advertisementData, null, 4));
           Ti.API.info('RSSI: ' + JSON.stringify(RSSI, null, 4));
       };
       
       function didConnectPeripheral(central, peripheral) {
           Ti.API.info('didConnectPeripheral');
           Ti.API.info('peripheral: ' + JSON.stringify(peripheral, null, 4));
       };
       
       function didFailToConnectPeripheral(central, peripheral, error) {
           Ti.API.info('didFailToConnectPeripheral');
           Ti.API.info('peripheral: ' + JSON.stringify(peripheral, null, 4));
           Ti.API.info('error: ' + JSON.stringify(error, null, 4));
       };
       
       function didDisconnectPeripheral(central, peripheral, error) {
           Ti.API.info('didDisconnectPeripheral');
           Ti.API.info('peripheral: ' + JSON.stringify(peripheral, null, 4));
           Ti.API.info('error: ' + JSON.stringify(error, null, 4));
       };
       
       /* */
       
       exports.scan = scan;
       
  7. Joe Falcone 2017-11-03

    and here is app.js
       /*
        * Single Window Application Template:
        * A basic starting point for your application.  Mostly a blank canvas.
        *
        * In app.js, we generally take care of a few things:
        * - Bootstrap the application with any data we need
        * - Check for dependencies like device type, platform version or network connection
        * - Require and open our top-level UI component
        *
        */
       
       //bootstrap and check dependencies
       if (Ti.version < 1.8) {
         alert('Sorry - this application template requires Titanium Mobile SDK 1.8 or later');
       }
       
       // This is a single context application with multiple windows in a stack
       (function() {
         //render appropriate components based on the platform and form factor
         var osname = Ti.Platform.osname,
           version = Ti.Platform.version,
           height = Ti.Platform.displayCaps.platformHeight,
           width = Ti.Platform.displayCaps.platformWidth;
       
         //considering tablets to have width over 720px and height over 600px - you can define your own
         function checkTablet() {
           var platform = Ti.Platform.osname;
       
           switch (platform) {
             case 'ipad':
               return true;
             case 'android':
               var psc = Ti.Platform.Android.physicalSizeCategory;
               var tiAndroid = Ti.Platform.Android;
               return psc === tiAndroid.PHYSICAL_SIZE_CATEGORY_LARGE || psc === tiAndroid.PHYSICAL_SIZE_CATEGORY_XLARGE;
             default:
               return Math.min(
                 Ti.Platform.displayCaps.platformHeight,
                 Ti.Platform.displayCaps.platformWidth
               ) >= 400;
           }
         }
       
         var isTablet = checkTablet();
         console.log(isTablet);
       
         var Window;
         if (isTablet) {
           Window = require('ui/tablet/ApplicationWindow');
         } else {
           // Android uses platform-specific properties to create windows.
           // All other platforms follow a similar UI pattern.
           if (osname === 'android') {
             Window = require('ui/handheld/android/ApplicationWindow');
           } else {
             Window = require('ui/handheld/ApplicationWindow');
           }
         }
         new Window().open();
         
         // pulled from iphone/ or android/ into toplevel depending on OS
         var CentralBLE = require('centralble');
         CentralBLE.scan();
         
       })();
       
       
  8. Joe Falcone 2017-11-03

    I put all 3 of these files in the top level (Resources). That's it for the code files.
  9. Joe Falcone 2017-11-03

    As I noted before, this code works when it is integrated into Hyperloop-examples which is an Alloy app.
  10. Hans Knöchel 2018-08-22

    So we investigates this internally and I even ran into this myself while trying to setup a test app. The reason it does does not work is that you need to import a Hyperloop-related class in order to trigger the Hyperloop compiler. Since there is no case where you would define a class without assigning it to a native class (e.g. as a delegate in 90 %), this does not represent an issue that should block anyone. An example to resolve this:
        / ES6+
        import { UIButton } from 'UIKit';
        
        // ES5
        const UIButton = require('UIKit/UIButton');
        
  11. Joe Falcone 2018-08-22

    So importing any random class will do the trick and "fix" this problem. Thanks for pointing this out.
  12. Hans Knöchel 2018-08-23

    [~jfalcone] I am wondering what you do with the created native class if not passed to any other Hyperloop based API (class/method/property). I personally only used it for subclassing / delegate-handling so far, but if there is a case where you would use it without that, we can make that happen as well. A simple change would be to scan for the "Hyperloop.defineClass" call like we do for the specific require/import statements already. All in for better UX!
  13. Kon Lovett 2018-08-23

    (define) var CentralManagerDelegate = Hyperloop.defineClass('CentralManagerDelegate', 'NSObject', ['CBCentralManagerDelegate']); ... module.exports = CentralManagerDelegate; (use) var CentralManagerDelegate = require('centralmanagerdelegate'); at no point is the BLE code directly associated w/ any native UI. so BLE is not a "Hyperloop based API"?
  14. Joe Falcone 2018-08-23

    What my colleague is saying is that there are situations where you would be using classes that may not be associated with native UX classes such as artificial intelligence/neural networks, data mining, communications and the like.
  15. Jan Vennemann 2018-08-27

    [~weevil], [~jfalcone], i think there might be a misunderstanding here. At some point you will do
        var CBCentralManager = require('CoreBluetooth/CBCentralManager');
        var CentralManagerDelegate = require('centralmanagerdelegate');
        var cenManDel = new CentralManagerDelegate();
        var centralManager = CBCentralManager.alloc().initWithDelegateQueueOptions(cenManDel, null, null);
        
    The first line is your native class usage that will trigger Hyperloop. It is not required to use any UI related classes, just any native class will do. Hans' code using the UIButton was only an example for a native class usage. I hope this clears things up :)

JSON Source