Blog tech

Introduction à RubyMotion

Rédigé par Nicolas Cavigneaux | 23 mai 2013

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.

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 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.

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 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.

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.

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.

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 :

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

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.

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”.

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.

Label heure courante

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.

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 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.