Cet article est publié sous licence CC BY-NC-SA
Dans cet article à propos de RubyMotion, nous allons voir comment mettre en place un UITableView pour y présenter des données. Nous utiliserons comme base l’application de todo développée à l’occasion de l’article précédent.
Vous pouvez donc récupérer le code sur GitHub et vous placer sur le commit “54a3b47331” qui correspond à l’état dans lequel nous avons laissé l’application à la fin de l’article précédent.
L’idée derrière cet article est d’améliorer la liste des tâches actuelle pour la remplacer par une tableview. Nous allons également ajouter la possibilité de supprimer une tâche depuis la table.
Avant toute chose, notez que l’application a été commencée sous iOS 6 qui affiche la barre de statut (opérateur, heure, état de la batterie) au dessus de la fenêtre d’application. Ce n’est plus le cas dans iOS 7 que nous allons maintenant utiliser. Sous iOS 7 cette barre de statut est comprise dans l’application et on se retrouve donc avec une en-tête qui chevauche la barre de statut. J’ai donc ajouté une méthode au contrôleur TaskViewController
permettant de la masquer :
def prefersStatusBarHidden
true
end
Les tables sont très utilisées dans les applications iOS car elles fournissent un moyen efficace de présenter de l’information de manière organisée et permettent de scroller facilement pour parcourir ces informations.
Pour rappel nous voulons présenter une liste de tâches à réaliser, ce besoin est donc particulièrement adapté à l’utilisation d’une UITableView. Nous allons donc commencer par modifier notre contrôleur ListViewController
pour qu’il se base sur un UITableViewController
plutôt que sur un simple UIViewController
que nous utilisions précédemment.
Cette simple modification de l’héritage nous permet de préciser à notre contrôleur qu’il devra se comporter comme une interface à un élément de type table. Il faut supprimer notre méthode loadView
puisque les UITableViewControllers créent leur propre vue personnalisé. Finalement il faut initialiser ce contrôleur via la méthode initWithStyle
spécifique aux UITableViewControllers.
On se retrouve donc avec le code suivant app/controllers/list_view_controller.rb
:
class ListViewController < UITableViewController
end
qui suffit à mettre en place l’écran suivant :
Nous n’avons donc pour le moment aucune information affichée. Cela semble logique vu que nous n’avons pas décrit à notre UITableViewController
comment récupérer les informations à afficher. Nous avons également perdu notre en-tête. Nous allons donc régler ces deux points.
Le meilleur moyen de mettre à jour notre table est de le faire au moment où elle apparaît sur l’écran, nous allons donc utiliser la méthode viewWillAppear
pour demander un rafraîchissement des données de la table :
class ListViewController < UITableViewController
def viewWillAppear(animated)
loadTodos
end
private
def loadTodos
@tasks = Task.list
self.tableView.reloadData
end
end
Nous appelons donc notre méthode privée loadTodos
chaque fois que la table view apparaît à l’écran ce qui nous permet de charger la liste des tâches à jour puis de forcer un rafraîchissement de la table.
Vous noterez que ce n’est pas suffisant pour réellement afficher les données dans la table. Il faut maintenant écrire les méthodes (protocole UITableViewDataSource
) qui vont permettre au UITableViewController
de savoir comment utiliser et afficher ces données.
La première méthode déléguée que nous allons mettre en place est la méthode tableView:numberOfRowsInSection:
qui permet de préciser au contrôleur le nombre de lignes à afficher dans la table. C’est donc très simple à implémenter :
def tableView(tableView, numberOfRowsInSection:section)
@tasks.size
end
Il suffit de retourner le nombre d’éléments que contient notre variable d’instance @tasks
. Nous pouvons maintenant passer à la méthode tableView:cellForRowAtIndexPath:
qui va retourner la cellule (UITableViewCell
) associée à une ligne donnée. Il est à noter qu’il est possible, pour des raisons de performances, de ré-utiliser les cellules au fil des affichages. Nous allons donc mettre en place ce système de cache qui serait particulièrement bénéfique dans une très grosse table :
CELL_REUSE_ID = "TaskCellId"
def tableView(tableView, cellForRowAtIndexPath:indexPath)
cell = tableView.dequeueReusableCellWithIdentifier(CELL_REUSE_ID) || UITableViewCell.alloc.initWithStyle(UITableViewCellStyleSubtitle, reuseIdentifier: CELL_REUSE_ID)
task = @tasks[indexPath.row]
cell.textLabel.text = task.name
cell.detailTextLabel.text = [task.created_at.strftime("%d/%M/%Y"), task.priority].join(" - ")
cell
end
En premier lieu, nous déclarons une constante qui nous servira pour le système de cache des cellules.
Ensuite nous implémentons la méthode tableView:cellForRowAtIndexPath:
.
La première ligne permet de rechercher la cellule correspondante en cache (tableView.dequeueReusableCellWithIdentifier(CELL_REUSE_ID)
), si elle existe elle sera utilisée, sinon on va allouer et initialiser une cellule en utilisant le style UITableViewCellStyleSubtitle
et en précisant son identifiant de ré-utilisation via le paramètre reuseIdentifier
.
La seconde ligne sert simplement à stocker la tâche correspondant à la cellule courante dans une variable locale task
. On récupère donc dans notre tableau @tasks
la tâche à l’index correspondant à la cellule courante.
La troisième ligne permet de définir le texte principal de la cellule, on décide ici d’utiliser le nom de la tâche.
La quatrième ligne permet quant à elle de définir le texte détaillé de la cellule, ici nous utilisons la date de création de la tâche ainsi que sa priorité.
Finalement il ne nous reste plus qu’à retourner la cellule pour qu’elle soit utilisée par la table.
Nous obtenons désormais une présentation bien plus agréable que celle que nous avions au départ :
Comme je l’ai signalé plus tôt, lors de la transition d’un UIViewController
vers UITableViewController
nous avons perdu notre en-tête avec le nom de l’application. Voyons comment remédier à cela en utilisant l’en-tête de table view en passant par la méthode tableView:viewForHeaderInSection
:
def tableView(tableView, viewForHeaderInSection:section)
# Ajout d'un en-tête
headerImageView = UIImageView.alloc.initWithFrame([[0, 0], [320, 60]])
headerImageView.image = UIImage.imageNamed("bgHeader.png")
# 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)
headerImageView.addSubview(headerTitle)
headerImageView
end
def tableView(tableView, heightForHeaderInSection:section)
60
end
Dans ce morceau de code, on retrouve quasiment le même code que celui que nous utilisions dans la version précédente pour générer l’en-tête à ceci près que la vue contenant le texte est ajoutée comme sous-vue de l’image plutôt que comme sous-vue de la vue principale. Il y a une raison toute simple à cela, nous devons retourner un unique élément à la fin de cette méthode pour qu’il soit ajouté à l’en-tête du tableau. Nous renvoyons donc une vue composée.
Vous noterez qu’on a également ajouté une seconde méthode tableView:heightForHeaderInSection:
qui permet de définir la hauteur de l’en-tête. Sans cette précision, l’en-tête utiliserait une valeur par défaut qui ne convient pas dans notre cas.
Voici le résultat obtenu :
Plus sexy n’est-ce pas ?
Pour continuer l’amélioration de notre application, nous voulons être en mesure de détecter quand une ligne de la table est sélectionnée ce qui nous permettrait ensuite d’agir dessus pour par exemple supprimer la tâche associée. Il est à noter que la méthode tableView:didSelectRowAtIndexPath:
est appelée chaque fois qu’une ligne de notre table sera sélectionnée :
def tableView(tableView, didSelectRowAtIndexPath:indexPath)
p "colonne #{indexPath.row} sélectionnée"
end
Nous n’utiliserons pas cette méthode mais il est intéressant de savoir qu’elle est disponible. Si vous avez tout de même implémenté cette méthode, vous verez le message vous indiquer l’index de la ligne sélectionnée dans le REPL chaque fois que vous toucherez l’une d’elle.
Nous allons maintenant ajouter un bouton permettant de supprimer la ligne sélectionnée et activer l’interaction utilisateur dans l’en-tête. En effet, par défaut, l’en-tête ne réagira pas aux clicks ou autres interactions utilisateur. Si vous ajoutez un bouton, il ne sera donc jamais déclenché.
Créons une méthode permettant de générer le bouton :
def deleteButton
button = UIButton.buttonWithType(UIButtonTypeRoundedRect)
button.setTitle("X", forState:UIControlStateNormal)
button.frame = [[0, 0], [50, 50]]
button.addTarget(self,
action:"deleteSelectedCell",
forControlEvents:UIControlEventTouchUpInside)
button
end
Rien que nous n’ayons déjà vu ici. Nous créons un bouton pour lequel on définie le titre et la position. Il ne reste plus qu’à définir le nom de l’action qui sera appelée quand l’événement UIControlEventTouchUpInside
sera détecté.
Il nous faut ensuite ajouter ce bouton à notre en-tête, nous ajoutons donc les lignes de code suivante à la méthode tableView:viewForHeaderInSection:
juste avant de retourner l’en-tête :
headerImageView.addSubview(deleteButton)
headerImageView.setUserInteractionEnabled(true)
Il ne nous manque donc plus qu’à implémenter la méthode de suppression de l’item :
def deleteSelectedCell
selected = self.tableView.indexPathForSelectedRow
if selected
@tasks.delete_at(selected.row)
self.tableView.deleteRowsAtIndexPaths([selected],
withRowAnimation:UITableViewRowAnimationMiddle)
end
end
La première ligne nous permet de savoir quelle est la ligne actuellement sélectionnée dans la table. Si une ligne est sélectionnée, on supprime l’élément correspondant dans notre liste des tâches (via une méthode qu’on implémentera ensuite) puis on demande à la table de l’effacer de ses éléments en utilisant une animation de type UITableViewRowAnimationMiddle
.
Comme vous l’aurez sûrement remarqué la méthode deleteRowsAtIndexPaths
permet de supprimer plusieurs éléments d’un coup.
Finalement nous pouvons implémenter la méthode de suppression dans notre classe Task
:
def delete_at(index)
@@list.delete_at(index)
end
Pour terminer cet article nous allons mettre en place le bouton de retour à l’ajout de tâche ce qui permettra de naviguer à travers l’application. Commençons par ajouter la méthode dédiée à la création du bouton :
def backButton
button = UIButton.buttonWithType(UIButtonTypeRoundedRect)
button.frame = [[270, 0], [50, 50]]
button.setTitle("<-", forState: UIControlStateNormal)
button.addTarget(self,
action: "goBack",
forControlEvents: UIControlEventTouchUpInside)
button
end
Ce code est très proche de celui que nous avions déjà mis en place dans le dernier article. On crée donc notre bouton qu’on place à droite dans l’en-tête, on définit le titre puis on accroche ce bouton à l’action “goBack”.
Nous pouvons maintenant effectivement ajouter ce bouton à notre en-tête en ajoutant la ligne suivante à la méthode tableView:viewForHeaderInSection:
:
headerImageView.addSubview(backButton)
Finalement implémentons la méthode goBack
qui est exactement la même que celle développée dans l’article précédent :
def goBack
@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
Voici une démonstration de l’application finalisée :
Nous avons vu dans cet article comment mettre en place un UITableViewController
basique. Il est possible d’aller bien plus loin avec notamment l’utilisation de cellules personnalisées qui permettent d’avoir un rendu méconnaissable des tableviews.
Vous trouverez l’ensemble du code d’exemple cet article, découpé en commits sur GitHub.
L’équipe Synbioz.
Libres d’être ensemble.
Nos conseils et ressources pour vos développements produit.