Blog tech

RubyMotion - les tableviews

Rédigé par Nicolas Cavigneaux | 24 septembre 2013

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.

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

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

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

En-tête de table

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 ?

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

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’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

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

Conclusion

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.