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 :
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,