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.
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
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 :
Notre instance du contrôleur TaskViewController est donc maintenant notre contrôleur principal de l’application.
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.
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 :
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 :
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 :
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.
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.
Nous pouvons maintenant passer à l’écriture de notre modèle qui nous permettra de gérer les données saisies par l’utilisateur.
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.
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 :
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)
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.
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.
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 :
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 :
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.