+49 40 608 12 460 (Development)
+49 175 5611694 (Kommunikation)

Hyperloop – „Hallo Welt!“

Nachdem im letzten Artikel eine grundsätzliche Einführung in Hyperloop gegeben wurde, wollen wir in diesem Teil eine erste App bauen.

 

Schritt für Schritt klären wir die Migration von Objectiv-C zu Hyperloop-Javascript. Zum Endetesten wir die Performance mit einer etwas anspruchsvolleren App (Conway's Game of Life).

Auf jeden Fall benötigen wir einige Dinge vorweg, das sind:

  1. einen Mac von Apple,
  2. das iOS SDK 7.0,
  3. git für OSX,
  4. den JS-Paketmanager npm,

 

Wer noch nicht das Tor zu zeitgeistiger Programmierung aufgeschlagen hat, muss auf seinem Mac erst einmal auf Flughöhe bringen und einen github-Client sowie den nodeJS-Paketmanager npm installieren. Das geht über homebrew oder eben auch als fertiges Binary von der nodeJS-Seite.

 

Jetzt ist schon einmal das Fundament geschaffen und Hyperloop kann installiert werden:

sudo npm install -g \
   git://github.com/appcelerator/hyperloop.git

Da wir vermutlich/hoffentlich nicht mit Adminrechten unterwegs sind und /usr/local/bin wahrscheinlich nicht dem Nutzer (also dir) gehört, brauchts dieses sudo.

Falls das für Dich alles alte Hüte sind, Du einen GitHub-Account hast, dann kannst du das Hyperloop-Projekt auch in Dein Git forken und dann lokal weiterarbeiten:

# klone hyperloop lokal
git clone https://github.com/_DEIN_GITHUB_USERNAME_/hyperloop.git

# gehe in das neue Verzeichnis
cd hyperloop

# füge  das  appcelerator repo im Netz hinzu
git remote add appcelerator https://github.com/appcelerator/hyperloop.git

# lokale Abhängigkeiten befriedigen
npm install

# erweitere  PATH zu diesem lokalen hyperloop.
# Dann brauchst Du bei Änderungen kein `npm install`
sudo npm link

Nun ist die Einrichtung erledigt und es kann losgehen.

 

Zuerst muss ein Projektverzeichnis angelegt werden. Das Projekt kann prinzipiell in jedem Ordner des Rechners liegen. Wir können beispielsweise einen Ordner 'foo' im Heimatverzeichnis oder im Titanium-Workspace anlegen. In diesen bislang leeres Verzeichnis legen wir eine Datei 'app.hjs' an und öffnen diese Datei mit einem Texteditor (beispielsweise TextWrangler oder vi). In dem kleinen Beispiel bauen wir eine Button, dessen Klick wir abfangen. 

 

Nun geht es wirklich los und wir importieren einige Pakete in den ersten Zeilen des Quelltextes:

@import("Foundation");
@import("UIKit");
@import("CoreGraphics");

Die Syntax dieser CNI-Keywords sind genauer im Hyperloop-Wiki beschrieben.

Die Zeilen ensprechen in Objectiv-C  folgendem Schnipsel:

#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
#import <CoreGraphics/CoreGraphics.h>

Nachdem die Framework-Elemente eingebunden sind, holen wir uns das keyWindow aus UIApplication.

var keyWindow = UIApplication.sharedApplication().keyWindow;

Im klassischen ObjectivC entspricht dem:

UIWindow *window = [UIApplication sharedApplication].keyWindow;

In Hyperloop wird also die gewöhnliche Syntax für Klassen und Instanzen  und die Punktnotation für Objekteigenschaften verwendet.


Um eine  Methode der statischen Objective-C Methode aufzurufen, wird folgender Code genutzt:

UIApplication.sharedApplication();

Da UIApplication eine Klasse ist, wird das in in Objective-C so aufgerufen:

+[UIApplication sharedApplication];

Eine kleine Erklärung der Objectiv-C Syntax ist nötig: Das Pluszeichen + zeigt eine statische Klassenmethode an. Das Minuszeichen - kennzeichnet eine Instanzmethode.

Um eine Instanzmethode aufzurufen, muss man die Methode an der Instanz aufrufen.  (entweder durch eine Methode, die eine Instanz zurückgibt oder indem eine Instanz geschaffen wird).

Um eine Instanz zu schaffen, wird das Schlüsselwort new verwendet.

Als nächstes bauen wir eine UIView.

var view = new UIView();

Das Äquivalent in Objectiv-C sieht so aus:

UIView *view = [UIView new];

Zuweilen muss ein spezieller Konstruktor (ein sogenanntes init in Objective-C) direkt anrufen werden – zum Beispiel, um eine UIComponent  zu initialisieren wie die UIView mit einem Rahmen:

var view = UIView.alloc().initWithFrame(frame);

Jetzt können wir unser Frame bauen. Standardgemäß  wird die C-Funktion CGRectMake benutzt. Diese Funktion hat vier (Float-)Parameter – die da sind: x, y, width, height.

Die C- und die JS-Syntax ist in dem Fall gleich:

CGRectMake(0,0,100,200);

Jetzt können wir das zu einem Befehl zusammenfassen. Diese Zeile baut einen Button. Wie immer zuerst die JS, dann die Objective-C Variante:

var button = UIButton.alloc().initWithFrame(CGRectMake(110, 100, 100, 44));
UIButton *button = [[UIButton alloc] initWithFrame:CGRectMake(110,100,100,44)];

To invoke a selector like this in Hyperloop, use the initial name as the method name and any additional named parameters as arguments such as:

button.setTitle(title, state);

Das ist äquivalent zu diesem Schnipsel:

[button setTitle:title forState:state];

Wenn die API also einen String erwartet, dann kann in der Hyperloop-Welt einfach ein String verwendet werden. Es gibt also nur die vier JS-Datentypen: string, number, null und undefined.

OK, packen wir es zusammen und bauen den Titel des Buttons:

button.setTitle("Drück mich!", UIControlStateNormal);

Der zweite Parameter ist eine C-Konstante. Deshalb kann das einfach übernommen werden.

 

Nachdem die View gebaut ist, kommt der Controler an die Reihe. Das heisst wir müssen einen Klickhandler definieren.  In Objective-C wird einfach eine Methode des Controlers aufgerufen. In Hyperloop muss eine Delegate-Klasse definiert werden. Mit der Hyperloop-Spezialsyntax kann eine native Klasse gebaut werden. Diese Rolle übernimmt die @class Direktive.

@class('ButtonHandler', NSObject, [], [
   {
       name: 'buttonClick',
       returnType: 'void',
       arguments: [],
       action: function() {
           console.log('button was clicked');
       }
   }
]);

Der obige Code kreiert eine Klasse ButtonHandler, vom NSObject abgeleitet ist, kein Protokoll (3. Argument) implementiert. Wir bauen eine Methode.


Zusammengefasst baut das eine Klasse und macht sie in Javascript verfügbar. Jetzt wird die Klasse instanziiert:

var handler = new ButtonHandler();

Wenn in Objective-C einem Button ein EventListener zugeordnet werden soll, wird ein target  angefügt:

- (void)addTarget:(id)target action:(SEL)action forControlEvents:(UIControlEvents)controlEvents

In Hyperloop wird das wie folgt realisiert:

button.addTarget(handler, NSSelectorFromString('buttonClick'), UIControlEventTouchDown);

Beachtenswert ist der zweite Parameter. Er ist vom Typ SEL (selector). Um einen Jacascript-String in diesen Typ zu wandeln verwendet  Objective-C die C-Funktion NSSelectorFromString.


Bevor wir zum Ende kommen, muss der Button zur View  und der View zum Window addiert werden.

window.addSubview(view);
view.addSubview(button);

Huura! wir haben es geschafft und habe unsere erste Hyperloop-App. Hier die gesamte Quelltext:

@import("Foundation");
@import("UIKit");
@import("CoreGraphics");
var window = UIApplication.sharedApplication().keyWindow;
var view = new UIView();
view.frame = UIScreen.mainScreen().applicationFrame;
@class('ButtonHandler', NSObject, [], [
	{
		name: 'buttonClick',
		returnType: 'void',
		arguments: [],
		action: function() {
			console.log('clicked button');
		}
	}
]);
var handler = new ButtonHandler();
var button = UIButton.alloc().initWithFrame(CGRectMake(110, 100, 100, 44));
button.setTitle("Drück mich fest!", UIControlStateNormal);
button.setTitleColor(UIColor.darkTextColor(), UIControlStateNormal);
button.addTarget(handler, NSSelectorFromString('buttonClick'), UIControlEventTouchDown);
window.addSubview(view);
view.addSubview(button);

Um diesen Hyperloop-Quelltext zu kompilieren, brauchts dieser Zeile:

hyperloop package --src=foo --dest=build --name=foo --appid=com.foo --platform=ios --debug --launch

Wenn alles gut gegangen ist, startet der Emulator nach einiger Zeit:

Ein guter Test um sich von dem immensen Geschwindigkeitsgewinn zu überzeugen ist die Implementierung von Conway's Game of Life.

Auf meinem MacBookAir schafft die App  auf dem Simulator 64.7 Frames pro Sekunde. Ein Screencast ist also sinnfrei. ;-) Und dennoch hier ist er: 

function getNextState(x, y, alive) {
        var count = 0,
                xm1 = x > 0,
                xp1 = x+1 < xSize,
                ym1 = y > 0,
                yp1 = y+1 < ySize;

        if (xm1) {
                if (ym1 && cells[x-1][y-1].lastAlive) { count++; }
                if (cells[x-1][y].lastAlive) { count++; }
                if (yp1 && cells[x-1][y+1].lastAlive) { count++; }
        }
        if (xp1) {
                if (ym1 && cells[x+1][y-1].lastAlive) { count++; }
                if (cells[x+1][y].lastAlive) { count++; }
                if (yp1 && cells[x+1][y+1].lastAlive) { count++; }
        }
        if (ym1 && cells[x][y-1].lastAlive) { count++; }
        if (yp1 && cells[x][y+1].lastAlive) { count++; }

        return (alive && (count === 2 || count === 3)) || (!alive && count === 2);
}

 

Eine alternative Möglichkeit der Hyperloop-Entwicklung besteht darin, den HJS-Code in einem Module zu kompilieren, das dann in der gewohnten Ti.Current-Umgebung genutzt werden kann. Der nächste Artikel beschreibt das Verfahren.