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 :
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 :
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 :
Cliquez pour lire la vidéo
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.