Pré-requis:
Vidéo du résultat final:
Dans ce tutoriel, nous verrons comment construire une application qui permet la gestion d’une liste de tâches à réaliser – présentées dans un ‘UITableView’ – , sous forme de mémos éditables.
Les ‘tâches’ seront lues et enregistrées dans un fichier de propriétés, ce qui assurera leur pérennité entre les chargements de l’application.
L’application proposera une liste de tâches à partir de laquelle il sera possible:
Savoir utiliser un ‘protocole’ sous Swift pour les passages de données entre les classes des scènes.
Explication:
Nous allons programmer un protocole, dans la classe de la scène ‘ajouter/modifier’, qui va permettre à l’abonné, de recevoir les messages ‘ajouterTache’ et ‘modifierTache’ avec en paramètre, des données en provenance de la classe proposant le protocole.
Action 1 – Ouvrons le projet de départ et testons l’app: Post’TIM version swift 6.01 – depart
Remarquons que les modifications, ainsi que l’ajout d’une nouvelle tâche, ne sont pas enregistrés.
Astuce: Pour tester une scène du storyboard, non accessible par navigation, renseigner sa propriété ‘is initial View Controller‘.
Action 1.01 – Analysons le schéma de départ:
La classe UIView propose la méthode de classe animateWithDuration(). Cette méthode permet d’animer plusieurs des propriétés des classes dérivées de UIView. Par exemple, unUILabel.alpha, unUIImageView.center.y, …
La méthode animateWithDuration() utilise la technique des blocs pour soumettre, à un fil d’exécution (thread), les instructions d’animation.
Voici sa signature:
UIView.animateWithDuration(
_ duration: NSTimeInterval,
// delay: NSTimeInterval,
// options: UIViewAnimationOptions,
animations: () -> Void,
completion: ((Bool) -> Void)?
)
Voici un exemple d’utilisation:
logo.alpha = 0 UIView.animateWithDuration( 2.0, animations: { self.logo.alpha = 1 // il faut utiliser 'self.' dans un bloc }, // animations: completion: { terminee in // la signature de completion indique un paramètre de type Bool println("L'animation de logo.alpha est terminée") } // completion: ) // UIView.animateWithDuration
Action 1.1.1 – Ajoutons le code suivant, à la méthode viewDidLoad de la classe Intro:
titre1.alpha = 0; titre2.alpha = 0; logo.alpha = 0; logo.center.y = view.bounds.height // Placer le centre du logo au bas de l'écran. // logo.center.y = view.frame.height // Placer le centre du logo au bas de la view. // Rappel: Il faut toujours préciser le contexte des instances, dans un bloc. Par exemple, 'self.titre1'. UIView.animateWithDuration(1, delay: 0, options: UIViewAnimationOptions.CurveEaseIn, animations: { self.titre1.alpha = 1 }, completion: { terminee in UIView.animateWithDuration( 2.0, animations: { self.titre2.alpha = 1 self.logo.alpha = 1 self.logo.center.y = self.titre2.center.y + self.titre2.frame.height }, // animations: completion: { terminee in println("Animation de logo.alpha terminée") } // completion: ) // UIView.animateWithDuration } // completion: ) // UIView.animateWithDuration
Note: Il faut mettre en commentaire: self.performSegueWithIdentifier(« versTaches », sender: self) situé dans la méthode virewDidLoad()
Aide mémoire:
Dans un bloc de code, il faut toujours préciser le contexte des méthodes et des propriétés.
Par exemple,
self.titre1
self.uneMéthode()
Grand Central Dispatch (GCD) permet la gestion des fils d’exécution (threads).
Nous avons vu, dans d’autres projets, comment programmer l’exécution d’une fonction suite à un délai.
Par exemple,
passer de la scène d’introduction à la scène principale d’un projet.
Ceci était réalisé grâce à la classe NSTimer.
Il est possible d’obtenir le même résultat en utilisant les fonctions de Grand Central Dispatch (GCD).
Autre référence: RW
Action 1.1.2 – Ajoutons le code suivant, permettant de lancer un segue après un délai, à la méthode viewDidLoad de la classe Intro:
// Utilisation de GrandCentralDispatch (GCD) pour placer en attente un 'performSegueWithIdentifier' // Référence: http://www.raywenderlich.com/60749/grand-central-dispatch-in-depth-part-1 let delaiEnSecondes:UInt64 = 2 let delaiEnNanoSecondes = Int64(delaiEnSecondes * NSEC_PER_SEC) // nanoSec = 10e-9 sec let deltaTemps = dispatch_time(DISPATCH_TIME_NOW, delaiEnNanoSecondes) dispatch_after(deltaTemps, dispatch_get_main_queue(), { self.performSegueWithIdentifier("versTaches", sender: self) } ) // dispatch_after
Action 1.1.3 – Testons l’application
Le protocole UITableViewDataSource propose la méthode ‘commitEditingStyle‘.
Cette dernière permet, en autres choses, d’activer la suppression d’une cellule sur glissement horizontal.
Il suffit d’ajouter la méthode ‘commitEditingStyle‘ pour qu’un glissement, sur une UITableViewCell, propose des opérations sur la cellule.
Action 1.2 – Ajoutons la méthode suivante à la classe ‘VCNotes‘
func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) { if (editingStyle == UITableViewCellEditingStyle.Delete) { // Effacer, de tableauDesTaches, // l'élément correspondant à la cellule courante tableauDesTaches.removeObjectAtIndex(indexPath.row) // Enregistrer le tableau dans le fichier.plist updatePlist() // Actualiser le UITableView tableView.reloadData() } // FIN -> editingStyle == delete } // ### FIN -> commitEditingStyle
Action 1.2b – Testons le glissement (swap) sur une des cellules du UITableView:
Si nous exécutons l’application, nous allons remarquer que l’heure affichée des tâches ne correspond pas aux données du fichier ‘listeDesTaches.plist’. Celà s’explique par le fait qu’une date est localisée au fuseau horaire ‘UCT’ lors de sa conversion vers un NSString.
Note: Utiliser le code suivant pour afficher la date:
cell.date.text = (tableauDesTaches[indexPath.row][« date »]! as NSDate).description
L’utilisation de la classe ‘NSDataFormatter‘ va permettre d’utiliser le fuseau horaire de l’appareil lors de la conversion. De plus, il sera possible de programmer les éléments de date à afficher. Par exemple, le nom du jour (lundi) + le numéro du mois + HH:MM, …
Voir le site suivant pour les commandes de formatage d’une date.
Action 1.3 – Ajoutons le code suivant à la méthode ‘cellForRowAtIndexPath‘ de la classe ‘VCNotes‘
let uneDate = tableauDesTaches[indexPath.row]["date"]! as NSDate // 1 - Obtenir la date de la tâche courante let unFormateurDeDate = NSDateFormatter() // 2 - Créer un formateur de date unFormateurDeDate.dateFormat = "yyyy.MM.dd HH:mm" // 3 - Préciser le format de date désiré let dateMiseEnForme = unFormateurDeDate.stringFromDate(uneDate) // 4 - Obtenir, en chaine de caractères, la date formatée cell.date.text = dateMiseEnForme; // 5 - Afficher dans la cellule courante
Action 1.3b – Testons l’application et observons maintenant le format des dates.
Note: voir 1.5 en premier.
Il y a deux ‘UIButton‘s dans le modèle de la cellule personnalisée du projet.
Un déplacement ‘segue’ pourra donc être déclenché par le bouton ‘ajouter‘ ou le bouton ‘consulter‘.
Dans les deux cas, il y aura exécution de la méthode ‘prepareForSegue‘ et le bouton sélectionné sera passé en paramètre.
Nous ne recevrons pas la cellule où est survenu l’événement. Il ne sera donc pas possible d’interroger le UITableView sur la position de la cellule reçue.
Il est par contre possible d’obtenir la position d’une cellule en fonction du point d’encrage (origin) d’un des objets présent dans le UITableView.
Note: Par default, le point d’encrage d’un objet se trouve au centre de l’objet.
Action 1.4 – Ajoutons le code suivant à la méthode ‘prepareForSegue‘ de la classe ‘VCNotes‘.
let button = sender as UIButton let buttonFrame = button.convertRect(button.bounds, toView:self.tableView) let indexPath = self.tableView.indexPathForRowAtPoint(buttonFrame.origin)
La scène Notes possède trois liens de type ‘segue’.
Il n’y a qu’une seule méthode ‘prepareForSegue‘ par classe de scène.
Par contre, en vérifiant le paramètre ‘segue.identifier‘, il sera possible de déterminer la destination.
Voici un exemple:
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject if segue.identifier == "modifier" { // Pointer sur l'instance la scène de destination // Passer les données à la scène de destination println("Segue vers 'modifier' avec le détail de l'indice 'n' du tableau") } // versAjouter if segue.identifier == "detail" { // Pointer sur l'instance la scène de destination // Passer les données à la scène de destination println("Segue vers 'detail' avec le détail de l'indice 'n' du tableau") } // VersDetail ... } // prepareForSegue
Note: Tester cet exemple en laboratoire avec le contenu de 1.4.
Action 1.5 – Ajoutons le code suivant à la méthode ‘prepareForSegue‘ de la classe ‘VCNotes‘:
// Tester si 'segue' est = 'ajouter' if segue.identifier == "ajouter" { let vers = segue.destinationViewController as VCAjouterTache // ********************************************************************************* // À observer // TODO: - Action 3.4b - Segue Ajouter: Renseigner le délégué // --------------------------------------------------------------------------------- /// vers.delegate = self; // --------------------------------------------------------------------------------- vers.modeAjouter = true; NSLog("Transition vers %@", segue.identifier); } // segue.identifier == "ajouter" // Tester si 'segue' est = 'modifier' if segue.identifier == "modifier"{ let vers = segue.destinationViewController as VCAjouterTache // ********************************************************************************* // À observer // TODO: Action 3.5b - Segue modifier: Renseigner le délégué // --------------------------------------------------------------------------------- /// vers.delegate = self; // --------------------------------------------------------------------------------- vers.modeAjouter = false vers.detailTache = Tache(detail: tableauDesTaches[indexPath!.row] as NSDictionary) // sauvegarder la sélection courante pour le retour de modifierTache indiceTacheCourante = indexPath!.row; NSLog("Transition vers %@ avec indice: %d", segue.identifier, indexPath!.row); } // segue.identifier == "modifier" // Tester si 'segue' est = 'detail' if segue.identifier == "detail"{ let vers = segue.destinationViewController as VCDetail vers.detailTache = Tache(detail: tableauDesTaches[indexPath!.row] as NSDictionary) NSLog("Transition vers %@ avec indice: %d", segue.identifier, indexPath!.row); } // segue.identifier == "detail"
Action 1.5b – Faire les TODO: des fichiers VCDetail et VCAjouterTache.
Action 1.5b – Testons l’application
Note: Pour une explication détaillée de la mise en place d’un protocole, voir le laboratoire Aquarium.
Dans le projet de départ, la classe VCAjouterTache propose déjà le protocole ‘tacheDelegate‘ avec les méthodes ‘ajouterTache‘ et ‘modifierTache‘.
Nous utiliserons ce protocole pour retourner les données modifiées vers la classe de la scène Notes.
Action 2.1 – Observons le code suivant du fichier ‘VCAjouterTache’.
/// 2.1a - Déclaration du protocole protocol tacheDelegate { /** Retourne, au délégué du protocole tacheDelegate, la tâche à ajouter. :param: instance de la classe Tache */ func ajouterTache(detailInfo:Tache) /** Retourne, au délégué du protocole tacheDelegate, la tâche modifiée. :param: instance de la classe Tache */ func modifierTache(detailInfo:Tache) } // protocol tacheDelegate /// 2.1b - Propriété pour le delégué du protocole tacheDelegate. var delegate:tacheDelegate?
L’appel d’une méthode d’un protocole, implémentée dans la classe du délégué, se fait en utilisant la syntaxe suivante: delegate?uneMéthodeDuProtocole().
uneMéthodeDuProtocole() sera exécutée que si ‘delegate‘ n’est pas nil.
Action 2.2 – Observons le code suivant du fichier ‘VCAjouterTache’.
// Si en modeAjouter if modeAjouter { delegate?.ajouterTache(info) } // modeAjouter else // alors c'est 'modeModifier' { self.delegate?.modifierTache(info) } // modeModifier
Le protocole de la classe ‘VCAjouterTache’ est maintenant en place. Par contre, s’il n’y a pas de délégué, les méthodes du protocole ne seront pas exécutées.
Note: La numérotation passe à 3.x car les étapes suivantes sont réalisées dans la classe ‘VCNotes’.
Action 3.3– Observons le code suivant de la classe ‘VCNotes‘
class VCNotes: UIViewController, UITableViewDataSource, tacheDelegate { ...
Note: Il faut enlever le commentaire /* tacheDelegate */ dans le code source. Cela provoquera l’erreur ‘VCNotes’ non conforme au protocole ‘tacheDelegate’.
À partir de cette étape, il faut maintenant programmer les méthodes ‘ajouterTache’ et ‘modifierTache’ du protocole ‘tacheDelegate’ dans la classe ‘Notes’.
Action 3.4a – Ajoutons le code suivant au fichier ‘VCNotes.m’
func ajouterTache(info:Tache){ NSLog("ajouterTache, info = %@", info.tache); // Ajouter au tableauTaches les données reçues. tableauDesTaches.addObject(info.tacheToDictionary()) NSLog("tableauDesTaches ajouterTache = %@", info.tacheToDictionary()); // Actualiser le UITableView self.tableView.reloadData() // Enregistrer le tableau dans le fichier.plist updatePlist() } // ### FIN -> ajouterTache
Action 3.4b – Observons le code suivant du fichier ‘VCNotes.m’
vers.delegate = self;
Action 3.5a – Ajoutons le code suivant au fichier ‘VCNotes.m’
func modifierTache(info:Tache) { NSLog("retour de la méthode 'modifier', info = %@", info.tache); // Remplacer l'élément courant du tableauTaches par les données reçues. tableauDesTaches[indiceTacheCourante] = info.tacheToDictionary() NSLog("tableauDesTaches élément %d modifié = %@", indiceTacheCourante, tableauDesTaches); // Actualiser le UITableView tableView.reloadData() // Enregistrer tableauTaches dans le fichier 'listeDesTaches.plist' updatePlist() } // ### FIN -> modifierTache
Action 3.5b– Observons le code suivant du fichier ‘VCNotes.m’
vers.delegate = self
Action 3.6 – Testons l’application.
Note: Le code est déjà présent dans le projet de départ. Ce qui suit est un exercice de compréhension de la technique nécessaire pour être en mesure d’enregistrer des données sur l’appareil.
Aide mémoire:
Les fichiers d’une application, présents dans le dossier d’installation, sont accessible en lecture seulement. Pour qu’une application puisse les modifier, il faut les copier vers un des dossiers de l’utilisateur: Documents, Images, …
Étape initiale – Au besoin, copier les fichiers, qui seront modifiés par l’application, du paquet de l’installation (bundle) vers un dossier de l’utilisateur.
4.1 – Si non présent dans le dossier ‘Documents’, copier le fichier listeDesTaches.plist, livré avec l’application, vers de dossier ‘Documents’
// MARK: - Gestion de la listeDesTaches.plist /** Envoyer une copie du ficher 'listeDesTaches.plist' vers le dossier 'Document'. Raison: La version livrée avec l'app est non modifiable. */ func copierPlistVersDocuments() { let filemanager = NSFileManager.defaultManager() let documentsPath: AnyObject = NSSearchPathForDirectoriesInDomains(.DocumentDirectory,.UserDomainMask,true)[0] let destinationPath = documentsPath.stringByAppendingString("/listeDesTaches.plist") if !filemanager.fileExistsAtPath(destinationPath) { let fileForCopy = NSBundle.mainBundle().pathForResource("listeDesTaches",ofType:"plist") filemanager.copyItemAtPath(fileForCopy!,toPath:destinationPath, error: nil) println("La liste des tâches a été copiée dans le dossier 'mes documents'") } else{ println("La liste des tâches est déjà dans le dossier 'mes documents'") } } // copierPlistVersDocuments
4.2 – Lire les tâches sauvegardées à partir du dossier ‘Documents’
/** Lire le fichier 'listeDesTaches.plist' vers le tableau 'tableauDesTaches' */ func lirePlist() { let filemanager = NSFileManager.defaultManager() let documentsPath : AnyObject = NSSearchPathForDirectoriesInDomains(.DocumentDirectory,.UserDomainMask,true)[0] let destinationPath = documentsPath.stringByAppendingString("/listeDesTaches.plist") tableauDesTaches = NSMutableArray(contentsOfFile: destinationPath) NSLog("%@", tableauDesTaches); } // lirePlist()
Note: Il faudrait ajouter une validation sur la lecture du fichier des données.
4.3 – Écrire les tâches dans le dossier ‘Document’
/** Écrire le tableau 'tableauDesTaches' dans le fichier 'listeDesTaches.plist' */ func updatePlist() { let filemanager = NSFileManager.defaultManager() let documentsPath : AnyObject = NSSearchPathForDirectoriesInDomains(.DocumentDirectory,.UserDomainMask,true)[0] let destinationPath = documentsPath.stringByAppendingString("/listeDesTaches.plist") tableauDesTaches.writeToFile(destinationPath, atomically:true) } // updatePlist()
Note: Il faudrait ajouter une validation sur l’écriture des données dans le fichier plist.
Xcode propose des outils pour documenter, en ligne, les codes sources, les classes, les méthodes, les propriétés.
La documentation en ligne, des classes livrées avec Xcode, est accessible soit par la séquence alt+clic:
soit par l’inspecteur d’aide:
Il est aussi possible d’obtenir de la documentation sur une méthode ou un propriété d’une classe:
Il est possible de programmer le même genre de documentation en ligne pour les classes de nos projets.
Par exemple, sur complétion du code:
Documentation en ligne d’une classe personnalisée
Pour rédiger de la documentation en ligne, il suffit d’utiliser la syntaxe suivante:
/// Une description de ce qui suit …
var unePropriété:Int
/// Une description de ce qui suit …
var uneAutrePropriété:String
Note: Il faut utiliser trois ‘/’.
Note: Entre le commentaire et le code, il est possible d’insérer des lignes vides mais pas de commentaires standards: // Ceci est un commentaire standard.
/**
De la documentation
sur plusieurs
lignes…
*/
func uneMéthodeDocumentée() {}
Note: Il faut utiliser ‘/**’ pour ouvrir le commentaire.
/**
Transformer les propriétés de la classe Tache en format NSDictionary
:param: Aucun
:returns: Tache en format NSDictionary
Auteur: Alain Boudreault
Date: 2014.10.20
M-A-J: 2014.10.21 – Ajout de la documentation en ligne.
*/
func tacheToDictionary()
Note: Le mot clé est entre ::
Note: Il doit y avoir au moins une ligne vide entre les sections.
Résultat:
Précision:
Au moment d’écrire ce document (Xcode 6.1), la fonction de documentation en ligne, sous swift, n’était pas entièrement implémentée par Apple.
Plusieurs mots clés, pour les sous-sections, qui étaient disponibles en Objective-C ne le sont pas encore.
Cette situation devrait être corrigée dans les prochaines versions de Xcode.
En référence, voici une liste des mots clés, de sous-sections, disponibles en Objective-C
[table]
Section, Exemple
:brief:, :brief: Ceci est la description sommaire
:attention:, :attention: À l’usage exclusif des étudiants de TIM
:author:, :author: Alain Boudreault
:bug:,:bug: La méthode @b mangerDuPain retourne un lapin!
:copyright:,:copyright: 2014 – Alain Boudreault
:date: ,:date: 2014.10.27
:invariant:,:invariant: ..
:note:, :note: ..
:post:, :post: ..
:pre:,:pre: ..
:remarks:,:remarks: Voici des remarques …
:sa:,:sa: sa text
:see:,:see: see text
:since:,:since: depuis toujours
:todo: ,:todo: Corriger la méthode @b mangerDuPain
:version:,:version: 1.0
:warning:,:warning: Ici la voix des mistérons…
:result:,:result: result text
:return:,:return: return text
:returns:,:returns: returns text
:code::endcode:, Pour afficher un bloc de code
[/table]
Exemple,
Les marqueurs MARK:, TODO: et FIXME: servent à insérer des points d’ancrage dans le code source. Ces points d’ancrage permettent un accès rapide à un section de notre code. Après définitions, il sont accessible via le menu de saut (Jump Bar).
Voici le code de la classe ‘Tache’. Il est extrait du projet en cours.
// // Tache.swift // Post'TIM version swift // // Créé par Alain Boudreault le 20105.11.15 // Copyright (c) 2014 Alain Boudreault. All rights reserved. // // MARK: - Note importante! // ============================================================================================ // À l'usage exclusif des étudiants et étudiantes de // ============================================================================================ import Foundation // NOTE: un retour de chariot est requis entre les différentes sections de la documentation. /** Classe pour regrouper les informations d'une tâche. Propose un init permettant de construire une tâche à partir d'un NSDictionary :returns: une instance de la classe Tache. :param: aucun Auteur: Alain Boudreault Date: 2014.10.20 */ class Tache { /// La date de la tâche. var date:NSDate /// Le nom de la tâche. var tache:String /// L'importance de la tâche, une valeur entre 1...9 var importance:String /// Un texte décrivant la tâche. var description:String ///< Un texte décrivant la tâche. // Syntaxe non fonctionnelle sous Swift. /** Construire une tache vide. :param: aucun */ init () { self.date = NSDate() self.description = "" self.importance = "" self.tache = "" } // init /** Construire une tache à partir de données d'un dictionnaire. :param: detail:NSDictionary contenant les clés "date", "description", "importance" et "tache" */ init (detail:NSDictionary) { self.date = detail["date"] as NSDate self.description = detail["description"] as String // FIXME: Valider le contenu de detail["importance"] self.importance = detail["importance"] as String self.tache = detail["tache"] as String } // init /** Transformer les propriétés de la classe Tache en format NSDictionary :returns: Tache en format NSDictionary Auteur: Alain Boudreault Date: 2014.10.20 M-A-J: 2014.10.21 - Ajout de la documentation en ligne. */ func tacheToDictionary() -> NSDictionary { return NSDictionary(dictionary: ["tache":self.tache, "date":self.date, "importance":self.importance, "description":self.description]) } // tacheToDictionary() //TODO: Ajouter une méthode quiSuisJe() } // class Tache
Pour ajouter des icons à l’application, il suffit de faire glisser des images, de la bonne taille, dans le panneau ‘AppIcon’ du groupe ‘Images.xcassets’:
Note: Il y a une série d’images, pouvant servir d’icon, dans un des dossiers du projet de départ.
Résultat sur l’appareil:
Dans spotlight:
Le titre de l’application est tronqué, sur le téléphone, car il est trop long.
Voici comment le changer:
Résultat sur l’appareil:
Voici qui complète le laboratoire Post’TIM!
Post’TIM version swift 6.01 – solution
Post’TIM version swift 6.1 – solution