Introduction à RubyMotion

Synbioz

RubyMotion est un framework permettant d'écrire des applications iOS et OS X natives en Ruby. Le code Ruby est compilé exactement de la même façon que le serait du code Objective C. Toutes les API Cocoa sont donc disponibles nativement. Le code généré est conforme aux règles de l'Apple Store et n'a pas besoin d'embarquer un interpréteur.

RubyMotion est une évolution du projet MacRuby, initié par Laurent Sansonetti. MacRuby est un projet open source qui a été sponsorisé par Apple et qui a pour but d'écrire des applications natives pour OS X en Ruby. Laurent, via sa société HipByte, a fait évoluer ce projet dans un premier temps pour permettre le développement d'applications iOS natives en Ruby. Depuis sa version 2.0, il permet également d'écrire des applications OS X.

RubyMotion n'est toutefois pas un projet open source, il vous faudra une licence pour pouvoir obtenir le kit de développement, mais également avoir accès à un support ultraréactif. Il est à noter que les mises à jour sont gratuites.

Les autres intérêts majeurs de RubyMotion sont la gestion automatisée de la mémoire et surtout le REPL qui est en fait une console interactive IRB enrichie permettant de déboguer en live une application lancée dans le simulateur. On pourra par exemple cliquer sur un élément pour analyser ses propriétés, les modifier ou encore ajouter des vues à la volée. On a donc un framework nous permettant d'utiliser Ruby tout en conservant des performances dignes des applications écrites en Objective C avec en prime un environnement de développement léger et puissant.

Cet article va tenter de vous présenter les bases de RubyMotion. Il est le premier d'une série qui sera dédiée à l'écriture d'applications iOS en Ruby.

Cet article est publié avec l'aimable autorisation de Synbioz, l'article original peut être lu sur le blog de Synbioz : Introduction à RubyMotion.

Commentez

- Titre : Introduction à RubyMotion

- Auteur : Synbioz

- Parution : 1 juillet 2013

- Mise à jour : 27 août 2013

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2013 Synbioz. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. Création d'une application iOS

- I-A. Structure d'un projet

- II. REPL : Console interactive

- III. Création d'une fenêtre

- III-A. Utiliser une image de fond

- IV. Ajout d'un label

- IV-A. Label « Hello World »

- IV-B. Label heure courante

- V. Conclusion

- Remerciements

I - Création d'une application iOS

RubyMotion est livré avec un ensemble d'outils simplifiant la gestion d'un projet, sa compilation, son déploiement sur un iDevice ou encore son débogage. On a donc une commande motion qui permet notamment de créer le squelette d'un projet, de mettre à jour RubyMotion ou encore de créer un ticket de support.

Au sein du projet, un Rakefile est à notre disposition nous permettant de :

- compiler le projet pour le simulateur ou un device ;

- créer une archive .ipa pour distribution ;

- obtenir les variables de configuration du projet ;

- générer les ctags pour faciliter la navigation dans l'éditeur de texte ;

- lancer le simulateur ;

- lancer les batteries de tests ;

- compiler le code sous la forme de bibliothèque réutilisable.

Créons notre premier projet de test :

$ motion create HelloWorld
Create HelloWorld
Create HelloWorld/app/app_delegate.rb
Create HelloWorld/Rakefile
Create HelloWorld/resources/Default-568h@2x.png
Create HelloWorld/spec/main_spec.rb

Nous pouvons donc aller dans le répertoire nouvellement créé et vérifier la configuration par défaut

$ cd HelloWorld
rake config
background_modes       : []
build_dir              : "./build"
codesign_certificate   : "iPhone Developer: Nicolas Cavigneaux (8KBAAAKF4)"
delegate_class         : "AppDelegate"
deployment_target      : "6.1"
device_family          : :iphone
entitlements           : {}
files                  : ["./app/app_delegate.rb"]
fonts                  : []
framework_search_paths : []
frameworks             : ["UIKit", "Foundation", "CoreGraphics"]
icons                  : []
identifier             : "com.yourcompany.HelloWorld"
interface_orientations : [:portrait, :landscape_left, :landscape_right]
libs                   : []
motiondir              : "/Library/RubyMotion"
name                   : "HelloWorld"
prerendered_icon       : false
provisioning_profile   : "/Users/nico/Library/MobileDevice/Provisioning Profiles/338B4DF5-111F-498F-BD4A-AAAAAAAAAB.mobileprovision"
resources_dirs         : ["./resources"]
sdk_version            : "6.1"
seed_id                : "8ZBABAA36Y"
short_version          : "1"
specs_dir              : "./spec"
status_bar_style       : :default
version                : "1.0"
weak_frameworks        : []
xcode_dir              : "/Applications/Xcode.app/Contents/Developer"

On voit donc le certificat de signature (nécessaire au déploiement sur un iDevice) utilisé, le type de cible (ici « iphone » et iOS 6.1 minimum), les frameworks Cocoa chargés, l'icône qui sera utilisée pour présenter l'application, l'identifiant de l'application, les orientations supportées, le nom de l'application, le répertoire de ressources (images, sons…) ou encore le numéro de version.

I-A - Structure d'un projet

On peut maintenant ouvrir le projet dans un éditeur pour en apprendre plus. Commençons par le Rakefile :

# -*- coding: utf-8 -*-
$:.unshift("/Library/RubyMotion/lib")
require 'motion/project/template/ios'

Motion::Project::App.setup do |app|
# Use `rake config' to see complete project settings.
app.name = 'HelloWorld'
end

C'est à cet endroit qu'est défini le type de template à utiliser, ici « ios » qui détermine le framework et la cible du projet. On voit également que le nom de l'application est défini dans le bloc setup. On peut donc y définir des valeurs personnalisées pour chaque variable de configuration disponible.

Il y a ensuite le fichier app/app_delegate.rb qui est le point d'entrée de l'application :

class AppDelegate
def application(application, didFinishLaunchingWithOptions:launchOptions)
true
end
end

Par défaut, l'application ne fera qu'afficher un écran noir. C'est dans la méthode application que nous pouvons instancier notre fenêtre principale soit via du code Ruby, soit en chargeant un fichier InterfaceBuilder (.xib).

Vous pouvez donc compiler et lancer l'application dans le simulateur :

$ rake
Build ./build/iPhoneSimulator-6.1-Development
Compile ./app/app_delegate.rb
Create ./build/iPhoneSimulator-6.1-Development/HelloWorld.app
Link ./build/iPhoneSimulator-6.1-Development/HelloWorld.app/HelloWorld
Create ./build/iPhoneSimulator-6.1-Development/HelloWorld.app/Info.plist
Create ./build/iPhoneSimulator-6.1-Development/HelloWorld.app/PkgInfo
Copy ./resources/Default-568h@2x.png
Create ./build/iPhoneSimulator-6.1-Development/HelloWorld.dSYM
Simulate ./build/iPhoneSimulator-6.1-Development/HelloWorld.app
(main)>

Un binaire est généré dans le répertoire build puis le simulateur lance l'application. On obtient notre fameux écran noir dans le simulateur.

Le répertoire resources quant à lui ne contient qu'une image noire par défaut. Le répertoire spec contient lui un fichier de test d'exemple :

describe "Application 'HelloWorld'" do
before do
@app = UIApplication.sharedApplication
end

it "has one window" do
@app.windows.size.should == 1
end
end

Les tests utilisent la bibliothèque Bacon qui est un clone allégé de RSpec. Ils permettent de tester facilement, comme d'habitude en Ruby, les différents aspects de l'application que ce soit les vues, les contrôleurs ou encore les modèles.

Vous pouvez lancer les tests via rake spec, l'unique test par défaut ne passera pas. Il vérifie que l'application a bien une fenêtre (UIWindow) ce qui n'est pas le cas de notre application actuelle. Nous verrons plus en détail l'écriture des tests dans un prochain article.

Notez que vous pouvez créer de nouveaux répertoires dans app/ pour organiser votre code. Il sera donc courant dans les projets d'avoir les répertoires :

- app/views : vues personnalisées ( ex. : un TableView amélioré par vos soins) ;

- app/models : classes définissant des objets métier ;

- app/controllers : comme en Rails, ce sont les aiguilleurs qui permettent aux vues de communiquer avec les modèles ou les services externes.

II - REPL : Console interactive

Vous l'aurez peut-être remarqué en lançant la tâche rake, une fois l'application lancée dans le simulateur, le terminal nous rend la main dans une console interactive IRB qui nous permet d'inspecter et de manipuler l'application en cours d'exécution :

(main)> p self
main
=> main
(main)> alert = UIAlertView.new
=> #<UIAlertView:0x96735f0>
(main)> alert.message = "Hello World!"
=> "Hello World!"
(main)> alert.show
=> #<UIAlertView:0x96735f0>

Nous venons, depuis la console interactive, de créer et d'instancier une modale contenant le texte « Hello World » puis nous l'avons affichée. Elle est donc visible dans le simulateur !

Un outil dont vous ne pourrez plus vous séparer tellement il est pratique. Il n'existe aucun équivalent en Objective C ou dans XCode, cet outil est une merveille.

Si vous faites ⌘-clic sur la modale, vous verrez que self change pour devenir #<UITextEffectsWindow:0x9437340>. On peut de cette manière inspecter n'importe quel élément très facilement.

III - Création d'une fenêtre

Nous allons maintenant ajouter à notre application une fenêtre principale pour y afficher notre « Hello World ». Pour cela nous allons retourner dans le fichier app/app_delegate.rb pour instancier la fenêtre et l'associer à un contrôleur :

app/app_delegate.rb

class AppDelegate

def application(application, didFinishLaunchingWithOptions:launchOptions)
@window = UIWindow.alloc.initWithFrame(UIScreen.mainScreen.bounds)
@window.rootViewController = MainViewController.alloc.init
@window.makeKeyAndVisible
true
end

end

Tout d'abord nous créons une UIWindow en initialisant sa taille à celle de l'écran.

Une fois cette fenêtre créée, nous définissons le contrôleur qui lui est associé et qui servira à régir son comportement. Nous allons ici utiliser le contrôleur MainViewController que nous allons écrire par la suite.

Une fois la fenêtre créée, nous la marquons comme étant la fenêtre principale qui sera donc chargée automatiquement au lancement de l'application.

Pour finir, nous retournons true ce qui est nécessaire au lancement de l'application.

Voyons maintenant le contrôleur !

app/controllers/main_view_controller.rb

class MainViewController < UIViewController

def viewDidLoad
view.backgroundColor = UIColor.scrollViewTexturedBackgroundColor
end

end

Nous redéfinissons ici la méthode viewDidLoad qui est appelée automatiquement dès que la vue associée au contrôleur est chargée. Ce que nous faisons dans cette méthode est tout simple, en effet on ne fait que changer le fond de la fenêtre. Par défaut celle-ci est noire, elle utilisera une texture grise disponible par défaut sous iOS.

UIWindow avec texture de fond

Nous aurions pu utiliser une couleur pleine :

class MainViewController < UIViewController

def viewDidLoad
view.backgroundColor = UIColor.greenColor
end

end

Nous aurions dans ce cas un fond vert :

UIWindow avec couleur de fond

III-A - Utiliser une image de fond

Il est possible d'utiliser une image en background. Il faut tout d'abord mettre à disposition cette image dans le répertoire resources. Dans notre exemple, ce fichier s'appelle customBackground.jpg :

class MainViewController < UIViewController

def viewDidLoad
background_view = UIImageView.alloc.initWithFrame(UIScreen.mainScreen.bounds)
background_view.image = UIImage.imageNamed("customBackground.jpg")
self.view.addSubview(background_view)
end

end

UIWindow avec image de fond personnalisée

Nous commençons donc par instancier une vue pour l'image via une UIImageView que l'on cale sur la taille de l'écran. On instancie ensuite l'image en elle-même (UIImage) en utilisant son nom de fichier. Il ne nous reste finalement plus qu'à ajouter cette nouvelle vue à la vue principale.

Nous aurions pu utiliser loadView plutôt que viewDidLoad pour que l'image soit chargée avant même que la vue n'apparaisse, nous verrons cela dans l'exemple suivant.

IV - Ajout d'un label

Nous allons maintenant ajouter deux labels qui permettront d'afficher « Hello World » ainsi que l'heure au chargement de la vue. Commençons par le plus simple à savoir le label « Hello World ».

IV-A - Label « Hello World »

class MainViewController < UIViewController

def loadView
self.view = UIView.alloc.init

background_view = UIImageView.alloc.initWithFrame(UIScreen.mainScreen.bounds)
background_view.image = UIImage.imageNamed("customBackground.jpg")
self.view.addSubview(background_view)

helloLabel = UILabel.alloc.initWithFrame [[110, 30], [200, 50]]
helloLabel.backgroundColor = UIColor.clearColor
helloLabel.textColor = UIColor.whiteColor
helloLabel.text = "Hello World!"

self.view.addSubview(helloLabel)
end

end

Nous utilisons loadView pour préparer le style et les éléments de la vue, nous devons donc instancier nous-mêmes la vue associée au contrôleur. loadView se charge normalement de ça, mais puisque nous écrasons la méthode par défaut, il faut penser à le faire manuellement.

On définit le background comme précédemment. Vient ensuite la création du label, on commence donc par instancier le label (UILabel) en définissant sa taille. On utilise d'ailleurs ici un sucre syntaxique de RubyMotion. Notre [[110, 30], [200, 50]] correspond en fait à un CGMakeRect(110, 30, 200, 50). Le premier nombre correspond à la position en X, le deuxième en Y, le troisième à la largeur du label et le dernier à sa hauteur.

On s'assure que le fond du label soit transparent et que le texte soit en blanc. Finalement on définit le texte du label puis on l'ajoute à la vue principale.

UIWindow avec label

IV-B - Label heure courante

class MainViewController < UIViewController

def loadView
self.view = UIView.alloc.init

background_view = UIImageView.alloc.initWithFrame(UIScreen.mainScreen.bounds)
background_view.image = UIImage.imageNamed("customBackground.jpg")
self.view.addSubview(background_view)

helloLabel = UILabel.alloc.initWithFrame [[110, 20], [200, 50]]
helloLabel.backgroundColor = UIColor.clearColor
helloLabel.textColor = UIColor.whiteColor
helloLabel.text = "Hello World!"

self.view.addSubview(helloLabel)

timeLabel = UILabel.alloc.initWithFrame [[90, 60], [200, 50]]
timeLabel.backgroundColor = UIColor.clearColor
timeLabel.textColor = UIColor.whiteColor

# Initialisation d'un calendrier à la date / heure actuelles
calendar = NSCalendar.alloc.initWithCalendarIdentifier(NSGregorianCalendar)
date = NSDate.date
calendar.components(NSMinuteCalendarUnit, fromDate: date)

# Utilisation de la locale fr_FR pour formater les dates
fr_FR = NSLocale.alloc.initWithLocaleIdentifier "fr_FR"

format = NSDateFormatter.alloc.init
format.locale = fr_FR
format.setDateFormat("dd MMM yyyy - HH:mm")

# Conversion de la date en chaine
dateString = format.stringFromDate(date)

timeLabel.text = dateString

self.view.addSubview(timeLabel)
end

end

UIWindow avec date

La première partie est identique à l'exemple précédent.

Nous initialisons ensuite un calendrier (au format grégorien) puis nous récupérons la date courante. On définit la précision souhaitée pour la décomposition de la date puis on force la locale française pour obtenir des dates en français.

Une fois cela fait on peut définir le format de date souhaité en sortie. Il ne nous reste plus qu'à générer la chaîne puis à l'utiliser dans le label. Finalement le label est ajouté à la vue principale.

V - Conclusion

En quelques lignes de code, nous avons une application fonctionnelle. Certes elle n'est pas très utile, mais démontre la facilité d'écriture d'une application iOS (UI incluse) en RubyMotion. Nous aurions pu construire l'UI dans l'interface builder et l'importer dans le code ce qui nous aurait encore épargné des lignes de code mais nous verrons cela dans un article à venir.

Dans le prochain article sur RubyMotion, nous verrons comment gérer un formulaire basique, récupérer et traiter les entrées utilisateur.

Vous trouverez le code de cet article sur GitHub

Remerciements

Cet article est publié avec l'aimable autorisation de Synbioz, l'article original peut être lu sur le blog de Synbioz : Introduction à RubyMotion.

Nous tenons à remercier ClaudeLELOUP pour sa relecture attentive de cet article.