RubyMotion - les modèles et les contrôleurs multiples

Publié le 14 août 2013 par Nicolas Cavigneaux | mobile

Cet article est publié sous licence CC BY-NC-SA

Dans cet article à propos de RubyMotion nous allons aborder un aspect important de la structuration du code qui vous sera utile dans la majorité des applications que vous développerez. Il s’agit aujourd’hui de voir comment mettre en place une application multi-contrôleurs (grossièrement ayant plusieurs fenêtres), comment mettre en place des transitions entre ces contrôleurs mais aussi comment factoriser son code au travers de modèles qui seront utilisés dans les contrôleurs.

Pour illustrer ces mécanismes, nous allons développer une application de gestion de tâches, une todo-list.

Cette application sera toutefois finalisée dans le prochain article sur RubyMotion puisque pour cela nous aurons besoin d’un élément d’UI complexe qui mérite un article à lui seul.

Création de l’application

Commençons par créer le projet :

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

Nous allons maintenant créer un répertoire app/controllers pour y ajouter le ficher contrôleur de notre formulaire de création de tâches :

$ mkdir app/controllers
$ touch app/controllers/task_view_controller.rb

Formulaire de création d’une tâche

Ouvrez le projet dans votre éditeur de texte préféré et éditez le fichier nouvellement créé :

app/controllers/task_view_controller.rb

class TaskViewController < UIViewController

  private

  def loadView
    self.view = UIView.alloc.init
    self.view.backgroundColor = UIColor.redColor
  end
end

Nous pouvons maintenant éditer le fichier app_delegate.rb pour y instancier notre contrôleur :

app/app_delegate.rb

class AppDelegate
  def application(application, didFinishLaunchingWithOptions:launchOptions)
    task_view_controller = TaskViewController.alloc.init

    @window = UIWindow.alloc.initWithFrame(UIScreen.mainScreen.bounds)
    @window.rootViewController = task_view_controller
    @window.makeKeyAndVisible

    true
  end
end

Vous pouvez compiler le projet et le lancer :

$ rake

Vous obtiendrez donc votre fenêtre sur fond rouge :

UIWindow avec fond rouge

Notre instance du contrôleur TaskViewController est donc maintenant notre contrôleur principal de l’application.

Ajout des éléments d’UI

Maintenant que notre contrôleur est instancié, il nous faut afficher les éléments de formulaire dont nous aurons besoin pour ajouter une tâche. Nous allons en profiter pour agrémenter l’aspect visuel de notre fenêtre.

Fond et en-tête

Retournons dans notre fichier contrôleur pour construire notre layout :

app/controllers/task_view_controller.rb

class TaskViewController < UIViewController

  private

  def loadView
    layoutView
  end

  def layoutView
    # Initialisation de la vue
    self.view = UIView.alloc.init

    # Utilisation d'un gris léger en couleur de fond
    self.view.backgroundColor = UIColor.colorWithRed(0.902, green: 0.902, blue: 0.902, alpha: 1.0)

    # Ajout d'un en-tête
    @headerImageView = UIImageView.alloc.initWithFrame([[0, 0], [320, 60]])
    @headerImageView.image = UIImage.imageNamed("bgHeader.png")

    self.view.addSubview(@headerImageView)
  end
end

Nous avons modifié loadView pour qu’elle appelle la méthode layoutView qui va se charger de décorer notre fenêtre et y placer les éléments d’UI.

Dans la méthode layoutView nous initialisons notre vue pour ensuite définir une couleur de fond gris clair.

Finalement, nous nous servons d’une image comme en-tête. Cette image a préalablement été ajoutée au répertoire resources. Une version “@2x” est également présente pour être utilisée (automatiquement) sur les écrans “retina”.

Voici ce que nous obtenons :

UIWindow avec fond gris et en-tête

Ajoutons maintenant un titre à notre en-tête :

@headerTitle = UILabel.alloc.initWithFrame([[0, 0], [320, 50]])
@headerTitle.text = "RubyMotion Todo"
@headerTitle.color = UIColor.colorWithRed(0.702, green: 0.702, blue: 0.702, alpha: 1.000)
@headerTitle.backgroundColor = UIColor.clearColor
@headerTitle.textAlignment = UITextAlignmentCenter
@headerTitle.font = UIFont.fontWithName("AvenirNext-Bold", size: 25)

self.view.addSubview(@headerTitle)

Ce code a simplement été ajouté à la fin de la méthode layoutView. On crée donc une instance de UILabel qu’on place par dessus l’image d’en-tête. On définit notre texte, la couleur à utiliser et on s’assure que la couleur de fond du label soit bien transparente.

Finalement, on aligne le texte au centre du label puis on définit une police et une taille personnalisée. Vous pouvez obtenir la liste des polices disponibles via : UIFont.familyNames dans le REPL par exemple.

Le résultat :

Image d'en-tête avec titre

Champs du formulaire

Nous pouvons passer à l’ajout du champ texte pour la saisie du titre de la tâche. Ici encore nous allons personnaliser l’élément pour avoir un rendu plus attractif.

Toujours à la suite de notre méthode layoutView, nous ajoutons le code suivant :

@titleTextField = UITextField.alloc.initWithFrame([[10, 75], [300, 45]])

@titleTextField.background = UIImage.imageNamed("bgTextField.png")
@titleTextField.textColor = UIColor.colorWithRed(0.451, green:0.451, blue:0.451, alpha:1.0)
@titleTextField.font = UIFont.fontWithName("AvenirNextCondensed-DemiBold", size:25)

@titleTextField.textAlignment = UITextAlignmentCenter
@titleTextField.contentVerticalAlignment = UIControlContentVerticalAlignmentCenter

@titleTextField.placeholder = "Nom de la tâche"

@titleTextField.delegate = self

self.view.addSubview(@titleTextField)

Nous créons une instance d’UITextField pour laquelle nous définissons une image de fond qui a été ajoutée au répertoire resources. Nous personnalisons ensuite la couleur du texte, sa police et sa taille. Nous faisons en sorte que le texte soit centré horizontalement et verticalement dans le champ. Finalement, nous définissons un texte de substitution.

Nous préparons déjà le terrain pour la suite en déléguant ce champ texte à notre contrôleur puis nous l’ajoutons à notre vue.

Voici le résultat :

Champ texte personnalisé

Nous pouvons maintenant ajouter un sélecteur pour l’importance de la tâche “haute” ou “basse”. Pour ce faire nous allons utiliser un UISegmentedControl :

# Ajout d'un label pour la priorité
@prorityLabel = UILabel.alloc.initWithFrame([[10, 140], [300, 30]])
@prorityLabel.text = "Priorité"
@prorityLabel.color = UIColor.colorWithRed(0.400, green: 0.400, blue: 0.400, alpha: 1.0)
@prorityLabel.backgroundColor = UIColor.clearColor
@prorityLabel.font = UIFont.fontWithName("AvenirNext-DemiBold", size: 20)
@prorityLabel.textAlignment = UITextAlignmentCenter

self.view.addSubview(@prorityLabel)

# Ajout d'un sélecteur de priorité
@priorityValues = ["Bas", "Haut"]

@prioritySegmentedControl = UISegmentedControl.alloc.initWithItems(@priorityValues)
@prioritySegmentedControl.frame = [[10, 170], [300, 30]]
@prioritySegmentedControl.selectedSegmentIndex = 0

self.view.addSubview(@prioritySegmentedControl)

Nous créons un label pour désigner notre sélecteur à venir, rien que vous ne connaissiez pas déjà ici.

Nous passons ensuite à la création d’un tableau de valeurs qui seront disponibles dans le sélecteur. Nous initialisons ce UISegmentedControl en utilisant le tableau précédemment créé puis nous pré-sélectionnons le premier segment.

Il ne nous reste plus qu’à ajouter le contrôle à notre vue principale.

Sélecteur de valeur

Comme nous l’avons déjà vu dans les articles précédents, lorsqu’un champ texte est sélectionné, le clavier n’est ensuite jamais masqué quoi qu’on fasse. Ajoutons ce masquage de clavier à l’appuie sur “Entrée” :

def textFieldShouldReturn(textfield)
  textfield.resignFirstResponder
  return true
end

Ce morceau de code est à ajouter dans notre contrôleur avant la section private. Ce code s’assure de masquer le clavier quelque soit le champ texte ayant activé le clavier.

Gestion des tâches

Nous pouvons maintenant passer à l’écriture de notre modèle qui nous permettra de gérer les données saisies par l’utilisateur.

Modèle de gestion des tâches

Nous allons tout d’abord créer un répertoire pour accueillir notre modèle, puis créer notre fichier :

$ mkdir app/models
$ touch app/models/task.rb

Ce modèle sera très simple, il n’est là que pour illustrer la mise en place d’un modèle au sein d’un projet RubyMotion. Libre à vous de l’enrichir pour vos besoins ou d’en créer d’autres. Voici le notre :

app/models/task.rb :

class Task
  attr_accessor :priority, :name
  attr_reader :done, :created_at

  def initialize
    @done = false
    @created_at = Time.now
  end

  def toggle_status!
    @done = !@done
  end
end

Nous utilisons donc deux accesseurs pour pouvoir interagir sur la priorité et le nom depuis notre formulaire. Nous avons également deux attributs en lecture, l’un pour stocker le statut courant de la tâche et l’autre pour stocker sa date de création.

À l’initialisation de notre objet, nous fixons le statut (@done) à faux et nous remplissons la date de création avec l’heure courante.

Nous avons également une méthode d’instance qui va nous permettre de basculer l’état de la tâche de “non-fait” à “fait” et inversement.

Il ne nous reste plus qu’à utiliser ce modèle dans notre contrôleur.

Enregistrement d’une tâche

Il nous faut maintenant ajouter un bouton de validation pour notre formulaire et faire en sorte de créer une nouvelle instance de Task lorsqu’il sera cliqué.

Commençons par ajouter le bouton de validation :

app/controllers/task_view_controller.rb

  def layoutView
    

    @validateButton = UIButton.buttonWithType(UIButtonTypeRoundedRect)
    @validateButton.frame = CGRectMake(10, 400, 300, 40)

    @validateButton.setBackgroundImage(UIImage.imageNamed("btnValidate.png"), forState:UIControlStateNormal)
    @validateButton.setTitle("Valider", forState: UIControlStateNormal)
    @validateButton.setTitleColor(UIColor.whiteColor, forState: UIControlStateNormal)
    @validateButton.titleLabel.font = UIFont.fontWithName("AvenirNext-DemiBold", size: 20)

    @validateButton.addTarget(self,
                              action: "addTask:",
                              forControlEvents: UIControlEventTouchUpInside)

    self.view.addSubview(@validateButton)
  end

On crée donc une instance d’un UIButton à bords arrondis qu’on place en bas de l’écran sur presque toute la largeur. Pour embellir ce bouton nous utilisons une image de fond. On définit ensuite le titre du bouton, sa police et sa couleur.

Au passage on en profite pour binder le click sur le bouton à une action addTask qui reste à écrire. C’est elle qui se chargera de créer une instance de notre modèle Task et de définir ses attributs.

Finalement nous ajoutons notre bouton à la vue principale ce qui nous donne :

Bouton de validation personnalisé

Il nous faut maintenant écrire la méthode addTask mais, tout d’abord, il faut améliorer notre classe Task pour pouvoir y stocker les tâches créées mais également avoir un moyen de les récupérer, voici donc notre classe enrichie :

app/models/task.rb :

class Task
  @@list = []

  attr_accessor :priority, :name
  attr_reader :done, :created_at

  def initialize
    @done = false
    @created_at = Time.now

    @@list << self
  end

  def self.list
    @@list
  end

  def toggle_status!
    @done = !@done
  end
end

Nous avons ajouté une variable de classe pour stocker nos tâches (nous verrons la persistance dans un prochain article). À l’initialisation de l’objet nous l’ajoutons donc à notre tableau. Nous avons également ajouté une méthode de classe pour nous retourner les tâches qui ont été enregistrées.

Passons maintenant à l’écriture de la méthode addTask dans notre contrôleur :

app/controllers/task_view_controller.rb

def addTask(sender)
  task = Task.new
  task.name = @titleTextField.text

  priority_index = @prioritySegmentedControl.selectedSegmentIndex
  task.priority = @priorityValues[priority_index]

  @titleTextField.text = ""
  @prioritySegmentedControl.selectedSegmentIndex = 0

  NSLog("%@", Task.list.map { |task| "#{task.name} (#{task.priority})" }.join(", "))
end

La méthode qui reçoit l’événement doit prendre un paramètre sender qui lui permet de savoir d’où vient l’action.

Nous créons une instance de Task et nous définissons son nom en récupérant la valeur de notre champ texte.

Nous récupérons ensuite l’index actuellement sélectionné du contrôle segmenté puis nous nous servons de cet index pour récupérer la valeur correspondante dans notre tableau des valeurs de priorités.

Une fois notre objet rempli, nous pouvons vider le formulaire et nous en profitons également pour logguer les tâches actuellement disponibles dans notre variable de classe.

Faîtes plusieurs ajouts et vérifiez votre console, vous devriez y voir quelque chose de ce genre :

(main)> 2013-08-13 14:24:22.942 Todo[17738:c07] Foo (Haut)
(main)> 2013-08-13 14:24:30.681 Todo[17738:c07] Foo (Haut), Bar (Bas)

Affichage des tâches existantes

Nous allons maintenant mettre en place un nouveau contrôleur dont le but sera d’afficher notre liste. Cette première version de l’affichage sera très simple, elle reprendra le layout actuel et de simples labels pour l’affichage des différentes tâches. L’idée principale est ici de découvrir comment créer une transition d’un contrôleur à un autre.

Liste des tâches

Créons donc le fichier app/controllers/list_view_controller.rb :

class ListViewController < UIViewController

  def loadView
    layoutView
  end

  private

  def layoutView
    # Initialisation de la vue
    self.view = UIView.alloc.init

    # Utilisation d'un gris léger en couleur de fond
    self.view.backgroundColor = UIColor.colorWithRed(0.902, green: 0.902, blue: 0.902, alpha: 1.0)

    # Ajout d'un en-tête
    @headerImageView = UIImageView.alloc.initWithFrame([[0, 0], [320, 60]])
    @headerImageView.image = UIImage.imageNamed("bgHeader.png")

    self.view.addSubview(@headerImageView)

    # Ajout d'un titre à l'image d'en-tête
    @headerTitle = UILabel.alloc.initWithFrame([[0, 0], [320, 50]])
    @headerTitle.text = "RubyMotion Todo"
    @headerTitle.color = UIColor.colorWithRed(0.702, green: 0.702, blue: 0.702, alpha: 1.000)
    @headerTitle.backgroundColor = UIColor.clearColor
    @headerTitle.textAlignment = UITextAlignmentCenter
    @headerTitle.font = UIFont.fontWithName("AvenirNext-Bold", size: 25)

    self.view.addSubview(@headerTitle)

    # Ajout d'un label pour lister les tâches
    @taskNamesLabel = UILabel.alloc.initWithFrame([[10, 50], [300, 40]])
    @taskNamesLabel.color = UIColor.colorWithRed(0.400, green: 0.400, blue: 0.400, alpha: 1.0)
    @taskNamesLabel.backgroundColor = UIColor.clearColor
    @taskNamesLabel.text = Task.list.map { |task| "#{task.name} (#{task.priority})" }.join(", ")

    self.view.addSubview(@taskNamesLabel)
  end
end

Comme vous pouvez le noter, il y a de la duplication avec l’autre contrôleur, il aurait été judicieux de créer un helper qui se charge de définir la couleur de fond, l’image d’en-tête et son texte associé.

Une fois cette mise en forme achevée, nous créons un label qui nous permettra d’afficher une simple chaîne résultant de la concaténation des informations des tâches comme vu précédemment.

Transition entre les contrôleurs

Maintenant que notre contrôleur est prêt pour utilisation, il ne nous reste plus qu’à mettre en place la transition entre contrôleur au moment voulu. Dans notre cas, nous souhaitons basculer vers la liste des tâches à chaque ajout d’une tâche.

Nous allons donc tout naturellement faire cette transition dans la méthode addTask :

app/controllers/task_view_controller.rb

def addTask(sender)
  

  @listViewController = ListViewController.alloc.init
  @listViewController.view.frame = self.view.frame

  UIView.transitionFromView(self.view,
                            toView: @listViewController.view,
                            duration: 0.3,
                            options: UIViewAnimationOptionTransitionFlipFromLeft,
                            completion: nil)
end

Il est très simple de créer une transition, en effet il suffit d’instancier le contrôleur cible, de définir la taille de sa vue puis d’appeler la méthode de transition transitionFromView de UIView.

Cette méthode prend en premier paramètre la vue de départ, le paramètre toView définit la vue cible de la transition. On précise ensuite la durée de la transition, éventuellement le type de transition (ici nous avons choisi une transition de gauche à droite avec un axe de rotation au milieu de la vue) puis pour finir un éventuel callback de completion qui sera appelé une fois la transition terminée.

Pour finaliser, ajoutons un bouton de retour au formulaire pour pouvoir ajouter plusieurs tâches, la transition associées sera différente par souci de découverte :

app/controllers/list_view_controller.rb

def layoutView
  

  # Ajout du bouton de retour au formulaire
  @backButton = UIButton.buttonWithType(UIButtonTypeRoundedRect)
  @backButton.frame = CGRectMake(10, 400, 300, 40)

  @backButton.setBackgroundImage(UIImage.imageNamed("btnValidate.png"), forState:UIControlStateNormal)
  @backButton.setTitle("Ajouter une tâche", forState: UIControlStateNormal)
  @backButton.setTitleColor(UIColor.whiteColor, forState: UIControlStateNormal)
  @backButton.titleLabel.font = UIFont.fontWithName("AvenirNext-DemiBold", size: 20)

  @backButton.addTarget(self,
                        action: "goBack:",
                        forControlEvents: UIControlEventTouchUpInside)

  self.view.addSubview(@backButton)
end

Nous ajoutons ici un bouton, tout est classique, il est à noter qu’on bind ce bouton sur la méthode goBack que nous allons donc ajouter à notre contrôleur :

def goBack(sender)
  @taskViewController = TaskViewController.alloc.init
  @taskViewController.view.frame = self.view.frame
  UIView.transitionFromView(self.view,
                            toView: @taskViewController.view,
                            duration: 0.5,
                            options: UIViewAnimationOptionTransitionCurlDown,
                            completion: nil)
end

Encore une fois, rien de nouveau, nous instancions notre contrôleur contenant le formulaire puis nous créons une transition vers ce dernier. Nous avons juste changer la transition qui ressemblera ici à une page qui se déroule vers le bas.

Voici une démonstration de l’application finalisée pour mieux vous représenter les effets de transition :

Exercice pour le lecteur

Si vous souhaitez vous familiariser un peu plus avec ce que nous venons de découvrir, je vous propose de travailler à quelques améliorations simples. Vous pourrez donc :

  • afficher l’heure de création de la tâche au format français dans la liste
  • pré-remplir la liste avec quelques tâches au lancement de l’application
  • définir le contrôleur de liste comme contrôleur principal pour accéder à cette liste dès le lancement de l’application
  • supprimer le code dupliqué de génération du layout en passant par un helper

Conclusion

Nous avons vu dans cet article un aspect important dans la programmation en iOS qui consiste à passer d’un contrôleur à un autre en s’échangeant de l’information.

Vous trouverez l’ensemble du code d’exemple cet article, découpé en commits sur GitHub

Dans le prochain article concernant RubyMotion, nous verrons comment mettre en place une tableView qui représente l’un des éléments d’UI central dans la majorité des applications iOS. Contrairement au développement Web, les tables sont très utilisées que ce soient pour présenter des données ou pour mettre en forme une UI.

Nous nous attacherons donc à afficher nos tâches de manière plus détaillée et mieux organisée. Ça sera également l’occasion de mettre en place un mécanisme de suppression des tâches et de mise à jour de leur statut.


L’équipe Synbioz.

Libres d’être ensemble.