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 APIs 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 ultra-ré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’application iOS en Ruby.
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 :
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.
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 librairie Bacon qui est un clone allégé de RSpec. Il permettent de tester facilement, comme à l’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.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.
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.
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 :
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
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.
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”.
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ême 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.
Nous allons maintenant ajouter un label contenant la date / heure courante au moment du chargement de la vue ce qui nous permettra de découvrir la manipulation des dates à l’aide de Cocoa :
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 actuelle
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
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.
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 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
L’équipe Synbioz.
Libres d’être ensemble.