RubyMotion - les tableviews

Synbioz

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

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

Commentez ?

- Titre : RubyMotion - les tableviews

- Auteur : Synbioz

- Parution : 2 janvier 2014

- 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

- Note de transition

- I. Mise en place de la UITableView

- I-A. Affichage des données

- I-B. Entête de table

- I-C. Interagir avec la table

- II. Supprimer une tâche

- III. Retour à l'ajout de tâche

- IV. Conclusion

- V. Remerciements

Note de transition

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 un entê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

I - Mise en place de la UITableView

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ée. Enfin, 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 entête. Nous allons donc régler ces deux points.

I-A - Affichage des données

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 tableview 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 performance, 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 deuxième 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é.

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

I-B - Entête de table

Comme je l'ai signalé plus tôt, lors de la transition d'un UIViewController vers UITableViewController, nous avons perdu notre entête avec le nom de l'application. Voyons comment remédier à cela en utilisant l'entête de tableview en passant par la méthode tableView:viewForHeaderInSection :

def tableView(tableView, viewForHeaderInSection:section)
# Ajout d'un entête
headerImageView = UIImageView.alloc.initWithFrame([[0, 0], [320, 60]])
headerImageView.image = UIImage.imageNamed("bgHeader.png")

# Ajout d'un titre à l'image d'entê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

Nous retrouvons quasiment le même code que celui que nous utilisions dans la version précédente pour générer l'entê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'entête du tableau. Nous renvoyons donc une vue composée.

Vous noterez que l'on a également ajouté une méthode tableView:heightForHeaderInSection: qui permet de définir la hauteur de l'entête. Sans cette précision, l'entê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 ?

I-C - Interagir avec la table

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 permettra ensuite d'agir dessus, par exemple pour 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 l'implémentez tout de même, vous verrez le message vous indiquer l'index de la ligne sélectionnée dans le REPL chaque fois que vous toucherez l'une d'elles.

II - Supprimer une tâche

Nous allons maintenant ajouter un bouton permettant de supprimer la ligne sélectionnée et activer l'interaction utilisateur dans l'entête. En effet, par défaut, l'entête ne réagira pas aux clics 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 :

app/controllers/task_view_controller.rb

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éfinit 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 entête, nous ajoutons donc le code suivant à la méthode tableView:viewForHeaderInSection: juste avant de retourner l'entête :

headerImageView.addSubview(deleteButton)
headerImageView.setUserInteractionEnabled(true)

Il ne nous manque 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 que l'on implémentera par la suite) 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

III - Retour à l'ajout de tâche

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 que l'on place à droite dans l'entête, on définit le titre puis on accroche ce bouton à l'action goBack.

Nous pouvons maintenant effectivement ajouter ce bouton à notre entê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

IV - Conclusion

Voici une démonstration de l'application finalisée :

Animation en format "youtube" non supportée.
Contenu uniquement consultable en ligne à cette adresse :
https://www.youtube.com/v/UDwCM1bSi0U?version=3&feature=player_detailpage&rel=0

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 de cet article, découpé en commits, sur GitHub.

V - Remerciements

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