Cet article est publié sous licence CC BY-NC-SA
Dans le premier article à propos de RubyMotion nous avons vu les bases de la création d’une application avec l’utilisation de Rake puis du REPL. Nous avons ensuite mis en place l’affichage de labels, le traitement de date / heure ainsi que la personnalisation des backgrounds.
Nous avons vu qu’il très facile et assez concis de mettre en place le traitement d’informations mais aussi l’UI via RubyMotion.
Aujourd’hui nous allons nous pencher sur la mise en place du traitement d’un formulaire basique. L’idée est de permettre à l’utilisateur de sélectionner une heure ainsi qu’un fuseau horaire pour lui permettre de convertir cette heure vers le fuseau horaire concerné.
Voyons comment procéder.
Commençons par créer le projet :
$ motion create TimeZones
Create TimeZones
Create TimeZones/.gitignore
Create TimeZones/app/app_delegate.rb
Create TimeZones/Rakefile
Create TimeZones/resources/Default-568h@2x.png
Create TimeZones/spec/main_spec.rb
Nous allons, pour les besoins de cette présentation, demander à l’utilisateur d’entrer son nom, de choisir une heure et un fuseau horaire cible. Nous aurons donc besoin d’un champ texte, d’une liste de fuseaux horaires, d’un sélecteur de date et d’un label pour afficher le résultat.
Commençons par le plus simple, l’ajout du champ texte, du bouton de validation et du label.
Ouvrez le projet dans votre éditeur de texte préféré pour vous diriger dans le fichier app/app_delegate.rb
pour y instancier la fenêtre principale :
class AppDelegate
def application(application, didFinishLaunchingWithOptions:launchOptions)
@window = UIWindow.alloc.initWithFrame(UIScreen.mainScreen.bounds)
@window.rootViewController = RootViewController.alloc.init
@window.makeKeyAndVisible
end
end
Comme vous pouvez le voir, nous utilisons comme controller principal une instance de RootViewController
. Il va donc falloir créer cette classe :
app/controllers/root_view_controller.rb
class RootViewController < UIViewController
def viewDidLoad
view.backgroundColor = UIColor.scrollViewTexturedBackgroundColor
end
end
Vous pouvez déjà compiler le projet via la commande rake
et vous verrez apparaître la fenêtre principale avec en fond la texture grisée.
Nous pouvons maintenant passer à l’ajout du champ texte et de son label de description :
class RootViewController < UIViewController
def viewDidLoad
view.backgroundColor = UIColor.scrollViewTexturedBackgroundColor
view.addSubview name_label
view.addSubview name_text_field
end
private
def name_label
label = UILabel.alloc.initWithFrame [[10, 10], [100, 30]]
label.backgroundColor = UIColor.clearColor
label.textColor = UIColor.whiteColor
label.text = "Votre nom"
label
end
def name_text_field
textField = UITextField.alloc.initWithFrame [[110, 10], [170, 30]]
textField.borderStyle = UITextBorderStyleRoundedRect
textField.font = UIFont.systemFontOfSize(15)
textField
end
end
Nous avons ici ajouté deux méthodes privées qui nous permettent de générer un label avec fond transparent et texte blanc puis un champ texte avec une police de caractères à 15px.
Ces deux méthodes sont appelées l’une après l’autre au chargement de la vue pour que les éléments soient ajoutés à la vue principale.
Si vous testez vous verrez qu’il n’y a pour le moment pas grand chose de fonctionnel, d’ailleurs une fois le clavier virtuel déployé, impossible de revenir en arrière.
Traitons ce problème avant de passer à la suite.
def textFieldShouldReturn(text_field)
text_field.resignFirstResponder
end
La définition de la méthode textFieldShouldReturn
permet de définir ce qui doit se passer lorsque l’utilisateur demande explicitement la fermeture du clavier via un appuie sur “entrée” par exemple.
Il faudra ensuite préciser à qui nous déléguons cette tâche, en l’occurrence ce sera notre controller principal qui va s’en charger.
Nous allons donc modifier notre code pour qu’il fonctionne comme attendu :
class RootViewController < UIViewController
def viewDidLoad
view.backgroundColor = UIColor.scrollViewTexturedBackgroundColor
view.addSubview name_label
view.addSubview name_text_field
end
def textFieldShouldReturn(text_field)
text_field.resignFirstResponder
end
private
def name_label
label = UILabel.alloc.initWithFrame [[10, 10], [100, 30]]
label.backgroundColor = UIColor.clearColor
label.textColor = UIColor.whiteColor
label.text = "Votre nom"
label
end
def name_text_field
textField = UITextField.alloc.initWithFrame [[110, 10], [170, 30]]
textField.borderStyle = UITextBorderStyleRoundedRect
textField.font = UIFont.systemFontOfSize(15)
textField.delegate = self
textField
end
end
Nous avons donc ajouté le textField.delegate = self
dans la méthode name_text_field
qui permet de préciser le controller qui doit gérer le comportement du champ texte puis nous avons ajouté la méthode dédiée à la gestion de l’appui sur la touche “retour” dans laquelle nous demandons de fermer le clavier pour le champ texte à l’origine de la demande.
Pour peaufiner le comportement, il serait pratique de faire en sorte que le clavier se ferme également lorsque nous cliquons ailleurs que sur le clavier en lui même.
Pour arriver à nos fin nous allons attraper l’événement de “tap” simple sur l’écran. Lorsqu’un “tap” est effectué, nous fermerons le clavier si ce dernier est visible. Lors d’un “tap” nous n’aurons pas connaissance du champ texte en cours d’utilisation, nous allons donc devoir stocker notre champ texte dans une variable d’instance pour pouvoir y faire référence par la suite. Voici donc le code modifié :
class RootViewController < UIViewController
def viewDidLoad
view.backgroundColor = UIColor.scrollViewTexturedBackgroundColor
@text_field = name_text_field
view.addSubview name_label
view.addSubview @text_field
single_tap = UITapGestureRecognizer.alloc.initWithTarget(self, action: :'handle_single_tap')
view.addGestureRecognizer(single_tap)
end
def textFieldShouldReturn(text_field)
text_field.resignFirstResponder
end
def handle_single_tap
@text_field.resignFirstResponder
end
private
def name_label
label = UILabel.alloc.initWithFrame [[10, 10], [100, 30]]
label.backgroundColor = UIColor.clearColor
label.textColor = UIColor.whiteColor
label.text = "Votre nom"
label
end
def name_text_field
textField = UITextField.alloc.initWithFrame [[110, 10], [170, 30]]
textField.borderStyle = UITextBorderStyleRoundedRect
textField.font = UIFont.systemFontOfSize(15)
textField.delegate = self
textField
end
end
Nous avons donc ajouté la reconnaissance du “tap” simple sur notre vue et associé ce tap à la méthode handle_single_tap
qui va explicitement demander à notre champ texte de masquer son clavier s’il est visible.
Lorsque notre utilisateur valide sa saisie, nous allons ajouter un message dans un label pour le saluer. Ce label servira également à lui indiquer l’heure du fuseau horaire sélectionné par la suite.
Le code va être assez simple puisqu’en nous basant sur l’existant il ne nous reste qu’à récupérer le texte lors de la validation de la saisie par l’utilisateur pour l’ajouter à notre label nouvellement créé.
Nous ajoutons donc une méthode privée pour générer le label :
def remote_time_label
label = UILabel.alloc.initWithFrame [[0, 350], [view.frame.size.width, 30]]
label.backgroundColor = UIColor.clearColor
label.textColor = UIColor.whiteColor
label.textAlignment = NSTextAlignmentCenter
label
end
On prend ici soin de créer un label qui prend toute la largeur de la vue et d’aligner le texte au centre puis nous ajoutons cette vue à notre vue principale via viewDidLoad
:
@remote_time_label = remote_time_label
view.addSubview @remote_time_label
Nous pouvons maintenant faire en sorte que le label soit mis à jour suite à la validation utilisateur :
def textFieldShouldReturn(text_field)
text_field.resignFirstResponder
@remote_time_label.text = "Bonjour #{text_field.text} !"
end
Nous avons donc bouclé notre premier étape et nous allons pouvoir passer à l’étape suivante qui va consister à permettre à l’utilisateur de choisir son fuseau horaire cible.
Il va nous falloir récupérer une liste des fuseaux horaires disponibles pour ensuite les afficher dans une liste déroulante. L’utilisateur pourra ainsi faire son choix et on utilisera la valeur sélectionnée pour faire la conversion de l’heure.
Dans un premier temps concentrons nous sur la mise en place de cette liste déroulante pour simplement afficher le fuseau horaire sélectionné.
En tout premier lieu, à l’initialisation de la vue nous allons créer une variable d’instance qui contiendra les fuseaux horaires. Cocoa nous permet d’obtenir cette liste très facilement :
@timezones = NSTimeZone.knownTimeZoneNames
Nous pouvons ensuite ajouter une méthode privée qui nous servira à générer notre vue pour la liste déroulante :
def timezone_picker
picker = UIPickerView.alloc.init
picker.showsSelectionIndicator = true
picker.center = self.view.center
picker.dataSource = self
picker.delegate = self
picker
end
Nous initialisons donc un UIPickerView
, nous précisons que l’on souhaite avoir un indice visuel pour l’élément sélectionné. On place la liste déroulante au centre de la fenêtre puis on définit le délégué et la source de données. Une fois encore, c’est notre controller principal qui se chargera de ça.
Nous pouvons maintenant ajouter cette vue à la vue principale :
view.addSubview timezone_picker
Il ne nous reste plus qu’à implémenter les méthodes requises par l’interface de UIPickerView
:
def numberOfComponentsInPickerView(pickerView)
1
end
def pickerView(pickerView, numberOfRowsInComponent:component)
@timezones.size
end
def pickerView(pickerView, titleForRow:row, forComponent:component)
@timezones[row]
end
def pickerView(pickerView, didSelectRow:row, inComponent:component)
@remote_time_label.text = "#{@text_field.text}, vous avez choisi #{@timezones[row]}"
end
La méthode numberOfComponentsInPickerView
permet de définir le nombre de listes déroulantes qu’on aura au sein de la vue, dans notre cas nous n’avons qu’une liste à afficher.
La méthode pickerView(pickerView, numberOfRowsInComponent:component)
nous permet d’indiquer au composant le nombre total d’éléments qui seront dans la liste. Pour nous, c’est le nombre d’éléments dans notre tableau de fuseaux horaires.
La méthode pickerView(pickerView, titleForRow:row, forComponent:component)
permet de définir le contenu de chaque élément de la liste, le titre de l’élément en quelque sorte, on aura par exemple “Europe/Paris”. Nous devons donc tout simplement retourner l’élément de notre tableau à l’index demandé, ici disponible via la variable row
.
La méthode pickerView(pickerView, didSelectRow:row, inComponent:component)
permet quand à elle de définir le comportement à adopter lorsqu’une sélection est faite dans la liste. Nous décidons ici d’utiliser notre label pour y afficher le nom de l’utilisateur ainsi que le fuseau horaire choisi.
Voici un exemple du résultat obtenu :
La dernière étape en terme d’interaction avec l’utilisateur est la possibilité de lui laisser choisir la date et l’heure à convertir, pour cela nous allons utiliser un élément d’UI appelé UIDatePicker
.
Nous avons pour le UIPickerView
utilisé la taille par défaut, nous pourrions faire de même avec le UIDatePicker
mais tous les éléments ne tiendraient pas à l’écran. Nous allons donc devoir personnaliser la taille et le positionnement du UIPickerView
, les UIDatePicker
ne permettant pas ce genre de manipulation.
Nous ajoutons donc une méthode privée pour générer la vue dont nous avons besoin :
def date_picker
picker = UIDatePicker.alloc.init
picker.center = [view.frame.size.width / 2, 320]
picker
end
Il nous suffit ensuite de l’ajouter à la vue principale lors de son initialisation :
view.addSubview date_picker
Finalement nous devons modifier les méthodes existantes pour le UIPickerView
et notre label pour qu’ils ne se recouvrent pas les uns, les autres :
def remote_time_label
label = UILabel.alloc.initWithFrame [[0, 430], [view.frame.size.width, 30]]
label.backgroundColor = UIColor.clearColor
label.textColor = UIColor.whiteColor
label.textAlignment = NSTextAlignmentCenter
label
end
def timezone_picker
picker = UIPickerView.alloc.initWithFrame [[0, 50], [320, 120]]
picker.showsSelectionIndicator = true
picker.dataSource = self
picker.delegate = self
picker
end
Nous pouvons maintenant mettre en place un mécanisme similaire à celui du UIPickerView
pour réagir lors de la sélection d’une date par l’utilisateur. Pour cela nous allons devoir stocker notre sélecteur de date dans une variable d’instance puis observer ses événements pour savoir quand la valeur sélectionnée change.
Dans le viewDidLoad
, on aura :
@date_picker = date_picker
view.addSubview @date_picker
@date_picker.addTarget(self, action: :'handle_date_change', forControlEvents:UIControlEventValueChanged)
On demande ici a être prévenu à chaque changement de valeur dans le UIDatePicker
et que la méthode handle_date_change
soit appelée à ce moment là. Il ne nous reste donc plus qu’à implémenter cette méthode pour afficher la date sélectionnée :
def handle_date_change
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_picker.date)
@remote_time_label.text = dateString
end
On récupère donc la date via @date_picker.date
, date qu’on prend soin de formater pour l’affichage comme vu dans le précédent article. On l’affiche ensuite dans le label.
Dernière étape de cet article, convertir la date / heure choisie vers le fuseau horaire choisi juste au dessus. Nous allons donc devoir modifier notre méthode handle_date_change
. Pour avoir accès au fuseau horaire sélectionné à tout instance, nous allons stocker la vue dans une variable d’instance dans le viewDidLoad
:
@timezone_picker = timezone_picker
view.addSubview @timezone_picker
Nous pouvons maintenant modifier la méthode handle_date_change
:
def handle_date_change
selected_row = @timezone_picker.selectedRowInComponent(0)
selected_tz = @timezones[selected_row]
fr_FR = NSLocale.alloc.initWithLocaleIdentifier "fr_FR"
format = NSDateFormatter.alloc.init
format.locale = fr_FR
format.timeZone = NSTimeZone.timeZoneWithName(selected_tz)
format.setDateFormat("dd MMM yyyy - HH:mm")
dateString = format.stringFromDate(@date_picker.date)
@remote_time_label.text = dateString
end
Nous n’avons finalement pas modifié grand chose puisqu’on récupère simplement le fuseau horaire sélectionné puis on s’en sert ensuite sur notre formateur de date via format.timeZone = NSTimeZone.timeZoneWithName(selected_tz)
.
Pour finaliser le fonctionnement, on remplace le code de pickerView(pickerView, didSelectRow:row, inComponent:component)
pour qu’il appelle handle_date_change
. On a donc la date qui se met à jour qu’on change le fuseau horaire ou l’heure :
def pickerView(pickerView, didSelectRow:row, inComponent:component)
handle_date_change
end
Nous avons maintenant une application capable de convertir une heure donnée vers tous les fuseaux horaires !
Nous avons vu dans cet article plusieurs composants qui à eux seuls peuvent largement suffire à créer une application complète et fonctionnelle.
Il n’y a aucun piège à éviter, la seule chose bloquante peut être d’avoir à se rappeler des signatures des méthodes déléguées de UIPickerView
.
Vous trouverez le code de cet article sur GitHub
Dans le prochain article concernant RubyMotion, nous verrons comment mettre en place des models, des interactions multi-controllers. Nous découvrirons de nouveaux éléments d’UI que nous personnaliserons. Nous verrons également créer des transitions entre les différentes vues “root” des controllers.
L’équipe Synbioz. Libres d’être ensemble.
Nos conseils et ressources pour vos développements produit.