RubyMotion et les entrées utilisateur

Synbioz

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 images de fond.

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.

Cet article est publié avec l'aimable autorisation de Synbioz, l'article original peut être lu sur le blog de Synbioz : RubyMotion et les entrées utilisateur.

Commentez

- Titre : RubyMotion et les entrées utilisateur

- Auteur : Synbioz

- Parution : 18 juillet 2013

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2013 Synbioz. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. Création de l'application

- II. Formulaire de récupération du nom

- II-A. Masquage du clavier virtuel

- II-B. Mise à jour de l'affichage

- III. Conversion de l'heure

- III-A. Sélection du fuseau horaire

- III-B. Choix de l'heure

- III-C. Conversion vers le fuseau horaire choisi

- IV. Conclusion

- Remerciements

I - Création de l'application

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

II - Formulaire de récupération du nom

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
endclass 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 contrôleur 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
endclass 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
endclass 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.

II-A - Masquage du clavier virtuel

def textFieldShouldReturn(text_field)
text_field.resignFirstResponder
enddef 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 appui sur « entrée » par exemple.

Il faudra ensuite préciser à qui nous déléguons cette tâche, en l'occurrence ce sera notre contrôleur 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
endclass 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 contrôleur 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 lui-même.

Pour arriver à nos fins, 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
endclass 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.

II-B - Mise à jour de l'affichage

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
enddef 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@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 de l'utilisateur :

def textFieldShouldReturn(text_field)
text_field.resignFirstResponder
@remote_time_label.text = "Bonjour #{text_field.text} !"
enddef textFieldShouldReturn(text_field)
text_field.resignFirstResponder
@remote_time_label.text = "Bonjour #{text_field.text} !"
end

Nous avons donc bouclé notre première étape et nous allons pouvoir passer à la suivante qui va consister à permettre à l'utilisateur de choisir son fuseau horaire cible.

III - Conversion de l'heure

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

III-A - Sélection du fuseau horaire

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@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
enddef 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 contrôleur principal qui se chargera de ça.

Nous pouvons maintenant ajouter cette vue à la vue principale :

view.addSubview timezone_pickerview.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]}"
enddef 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 quant à 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 :

III-B - Choix de l'heure

La dernière étape en termes 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 utilisé pour le UIPickerView 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
enddef 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_pickerview.addSubview date_picker

Finalement, nous devons modifier les méthodes existantes pour le UIPickerView et notre label afin 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
enddef 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)@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
enddef 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.

III-C - Conversion vers le fuseau horaire choisi

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 instant, nous allons stocker la vue dans une variable d'instance dans le viewDidLoad :

@timezone_picker = timezone_picker
view.addSubview @timezone_picker@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
enddef 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é et 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 que l'on change le fuseau horaire ou l'heure :

def pickerView(pickerView, didSelectRow:row, inComponent:component)
handle_date_change
enddef 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 !

IV - Conclusion

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 comment créer des transitions entre les différentes vues root des controllers.

Remerciements

Cet article est publié avec l'aimable autorisation de Synbioz, l'article original peut être lu sur le blog de Synbioz : RubyMotion et les entrées utilisateur.

Nous tenons à remercier Didier Mouronval et Claude Leloup pour leur relecture attentive de cet article,