Programmez en Objective-C !
Last updated on Tuesday, January 8, 2013
  • 4 semaines
  • Moyen

Free online content available in this course.

Got it!

Les fichiers

On va à présent pouvoir attaquer les choses sérieuses : la manipulation des fichiers. Il y a un tas de choses à savoir, et ce chapitre va être long : représentation des chemins, différentes façons de lire un fichier, lire et enregistrer des objets, etc. Ce chapitre est dense et complexe. N'hésitez pas à prendre votre temps et à faire des expériences par vous-même. :)

Représentation d'un chemin

Vous vous souvenez sûrement de la fonction fopen(), utilisée en C pour ouvrir des fichiers. Elle prend en paramètre le chemin de ce fichier et le mode d'ouverture. C'est au chemin que l'on va s'intéresser ici.

En Objective-C, on peut utiliser deux objets pour représenter un chemin : une instance de NSString ou une instance de NSURL.

Un chemin comme une chaîne de caractères

Quand on utilise une chaîne pour représenter un chemin, on suit les mêmes règles que pour la fonction fopen() : un chemin commençant par / est un chemin absolu, le / représentant la racine du disque dur.

@"/usr/local/lib"

Ceci est un chemin absolu. Il représente le dossier lib se trouvant dans le dossier local, lui-même dans usr, qui se trouve à la racine.

Le chemin relatif indique l'emplacement d'un fichier ou d'un dossier en partant du dossier qui contient l'exécutable du programme. Si on change de place l'exécutable, il ne saura pas comment retrouver le fichier, contrairement au chemin absolu.

@"image/fond.png"

Ceci est un chemin relatif : il représente le fichier fond.png qui se situe dans le dossier image, lui-même dans le dossier contenant l'exécutable. Le chemin est donc relatif à la position de l'exécutable.

Voici un schéma pour illustrer tout ça :

Image utilisateur

On voit bien ici que le chemin absolu « part » de la racine disque, alors que le chemin relatif « part » de l'exécutable.

Mais si on part de l'exécutable, on ne peut pas remonter dans la hiérarchie des dossiers ?

Si ; voici un exemple : @"../Shared". Les .. indiquent le dossier parent. Dans l'exemple, notre exécutable se trouve dans le dossier prs513rosewood. .. est donc le dossier Users, qui est son dossier supérieur. Ainsi, Shared se trouve dans Users, au même niveau que le dossier prs513rosewood. On peut même mettre .. plusieurs fois dans un chemin : @"../../usr" représente le dossier usr situé à la racine. @"../../usr" est donc équivalent à @"/usr". Par contre, si on déplace l'exécutable, @"../../usr" va correspondre à autre chose (peut-être à rien). ^^

La classe NSURL

Dans le framework Foundation, il existe beaucoup d'objets en mesure de lire le contenu d'un fichier. NSString en est capable, par exemple. En regardant attentivement la documentation, on s'aperçoit que les méthodes permettant de lire le contenu d'un fichier attendent en paramètre une représentation du chemin de ce fichier. Certaines de ces méthodes demandent une simple chaîne de caractères, d'autres une instance de la classe NSURL.

Une URL est une chaîne de caractères représentant l'adresse de quelque chose (un fichier, un dossier, etc.). Vous en avez certainement déjà entendu parler, car c'est ce qui se trouve dans votre barre d'adresse et qui commence par http://www.siteduzero.com/....

Une instance de la classe NSURL peut donc représenter un fichier sur votre disque dur, ou encore une page web : c'est vous qui décidez. :)

Il existe deux méthodes de classe permettant de créer une nouvelle instance de NSURL :

  • URLWithString: : cette méthode crée une instance à partir d'une chaîne de caractères représentant une URL. On ne l'utilisera pas dans ce chapitre ;

  • fileURLWithPath: : cette méthode génère automatiquement une URL à partir d'un chemin passé en paramètre, comme on l'a vu tout à l'heure.

Ce qui donne :

NSURL * cheminURL = [NSURL fileURLWithPath:@"images/fichier.png"];

Vous devez maintenant savoir le faire ! :)

Lire et écrire du texte

On peut dire qu'en Objective-C, les processus de lecture et d'écriture dans un fichier sont beaucoup moins divers qu'en C. En effet, en C, on peut ouvrir un fichier en mode texte, récupérer son contenu ou encore écrire dans ce fichier, mais aussi le manipuler en mode binaire, en récupérer les données ou en écrire, tout cela avec très peu de fonctions « couteau suisse ». En effet, la fonction qui écrit un int dans un fichier est la même que celle qui écrit un double.

En Objective-C, il existe une fonction pour chaque variable « normale » (par exemple un int), mais aussi pour chaque objet. En fait, beaucoup d'objets du framework Foundation savent s'écrire eux-mêmes dans des fichiers.

C'est le cas de la classe NSString, même si son cas est particulier, car elle permet de lire et d'écrire dans un fichier en mode texte, en utilisant des caractères.

Initialiser une chaîne à partir d'un fichier

Cette fois-ci, on va étudier le prototype d'une fonction permettant de créer une chaîne à partir du contenu d'un fichier :

+ (id) stringWithContentsOfFile:(NSString *) path
                       encoding:(NSStringEncoding) enc
                          error:(NSError **) error

Cette méthode retourne un objet id, pas de surprise de ce côté-là. Le premier paramètre path est une chaîne de caractères représentant le chemin du fichier. Il suffit de donner une chaîne du type @"images/fichier.png". :)
Le deuxième paramètre est déjà plus intriguant. Il n'y a pas d'étoile entre les parenthèses, ce n'est donc pas un objet. En fait, NSStringEncoding représente un entier positif. Il faut donc fournir un nombre en paramètre pour indiquer le codage de caractères, c'est-à-dire la façon dont les caractères sont écrits dans le fichier.

Afin de simplifier la vie du développeur, des constantes ont été définies pour chacun de ces nombres. Ainsi, NSASCIIStringEncoding remplace l'entier 1, NSUTF8StringEncoding l'entier 4, etc.

Il est impératif de fournir un type de codage à l'appel de cette méthode.

Mais lequel choisir ?

La liste complète peut être trouvée ici. Pour la langue française, il est recommandé d'utiliser le codage Latin1 (NSISOLatin1StringEncoding) ou l'UTF-8 (NSUTF8StringEncoding). J'utiliserai ce dernier tout au long du tutoriel.

Le troisième paramètre n'est pas vraiment un objet non plus. Les deux étoiles indiquent que la méthode attend un pointeur sur un pointeur. o_O Comme son nom l'indique, cet objet error sert à récupérer les éventuelles erreurs lors de la lecture du fichier. Comme la méthode retourne un id (elle retourne nil si l'objet n'a pas pu être créé), on ne peut pas retourner un objet contenant l'erreur. Il faut donc donner l'adresse d'un pointeur pour que la méthode puisse modifier l'objet.

Il est pourtant toujours possible de modifier un objet passé en paramètre. Alors pourquoi passer l'adresse d'un pointeur ?

C'est vrai, mais pas avec cette fonction-là. Un schéma s'impose. :p Voici le fonctionnement de la fonction en cas d'erreur, si on donnait un objet en paramètre comme on le fait d'habitude.

Image utilisateur

Souvenez-vous, la valeur d'une variable est copiée lorsqu'elle est passée à une fonction.

Vous voyez le problème ? Le pointeur erreur passé en paramètre ne sera pas modifié par la fonction. Voici à présent ce qui se passe si on donne l'adresse du pointeur.

Image utilisateur

La fonction manipule le pointeur p_erreur_copie, ce qui correspond à manipuler erreur. Quand nous récupérons p_erreur, nous avons bien le même objet erreur que celui qui a été manipulé par la fonction. :)

Pour résumer, voici la méthode pour récupérer le contenu (et tout le contenu !) d'un fichier dans une NSString :

NSError * erreur = nil;
NSString * contenu = [NSString stringWithContentsOfFile:@"documents/fichier.txt"
                                               encoding:NSUTF8StringEncoding
                                                  error:&erreur]; // On donne l'adresse de "erreur"

Oui, on peut écrire un message sur plusieurs lignes, ça fait plus joli et c'est plus lisible pour les longs messages. :-°

Ainsi donc, notre chaîne de caractères contenu contient tout le fichier @"documents/fichier.txt". Cette méthode existe aussi en version init..., comme d'habitude.

Il existe une seconde méthode similaire accomplissant le même travail. Voici son prototype :

+ (id) stringWithContentsOfURL:(NSURL *) url
                      encoding:(NSStringEncoding) enc 
                         error:(NSError **) error

Comme vous le voyez, on n'est pas très loin de l'autre méthode, à l'exception du premier paramètre. Cette fois, on doit donner une instance de NSURL à la méthode. On peut donc initialiser cette instance à l'aide des méthodes vues tout à l'heure :

NSURL * cheminURL = [NSURL fileURLWithPath:@"documents/fichier.txt"];
NSString * contenu = [NSString stringWithContentsOfURL:cheminURL
                                              encoding:NSUTF8StringEncoding
                                                 error:NULL];
// On passe NULL en paramètre pour ne pas se préoccuper des erreurs

Voilà, vous savez lire un fichier texte ! :D

Tout cela est bien joli, mais tout un fichier dans une seule chaîne, ce n'est pas vraiment pratique ! Comment faire si on ne veut lire qu'une seule ligne ? Ou caractère par caractère ? Ou pour lire une chaîne formatée, comme avec fscanf() ?

L'avantage de cette technique, c'est qu'on n'a justement plus besoin de se soucier de ça ! Une fois la chaîne de caractères créée, il suffit de la parcourir pour se balader dans le fichier. Par exemple :

char c = 0;
int i;

for (i = 0 ; i < [contenu length] && c != '\n' ; i++) {
    c = [contenu characterAtIndex:i];
    // Du code...
}

Dans le cas présent, on lit la première ligne caractère par caractère. ;) Étant donné que la classe NSString propose beaucoup de méthodes très pratiques, on peut facilement manipuler le contenu d'un fichier.

Bon, je l'admets, on ne retrouve pas de fonction similaire à fscanf() dans la documentation de NSString. En revanche, si vous voulez vraiment analyser une chaîne formatée, je vous laisse regarder du côté de la classe NSScanner. ;)

Écrire dans un fichier

Bien entendu, si vous créez une instance de NSMutableString à partir d'un fichier et que vous la modifiez, le fichier restera inchangé. Pour mettre à jour le fichier, il faut écrire cette chaîne dans ce fichier. C'est possible grâce à la méthode que voici :

- (BOOL) writeToFile:(NSString *) path
          atomically:(BOOL) useAuxiliaryFile 
            encoding:(NSStringEncoding) enc
               error:(NSError **) error

La méthode retourne un booléen, qui vaut YES si l'écriture a pu se faire, et NO s'il y a eu un problème.

Vous devez certainement reconnaître les paramètres 1, 3 et 4 : ce sont les mêmes que lors de la lecture. Le second paramètre est intéressant. Si vous fournissez YES, l'écriture se fera dans un fichier distinct (c'est-à-dire différent de path), qui sera ensuite renommé pour remplacer path. Le fichier temporaire est donc effacé. Si vous fournissez NO, l'écriture se fait directement dans le fichier path. L'avantage de fournir YES est qu'en cas de crash inopiné, le contenu de path restera inchangé. :)

Comme pour la lecture, il existe la même méthode attendant une instance de NSURL :

- (BOOL) writeToURL:(NSURL *) url
         atomically:(BOOL) useAuxiliaryFile 
           encoding:(NSStringEncoding) enc
              error:(NSError **) error

Essayez de mettre cela en pratique ! :)

Enregistrer un objet, la théorie

Tout cela est bien joli, mais manipuler du texte n'est pas très pratique lorsqu'on souhaite enregistrer des objets dans un fichier. :-°

Deux solutions s'offrent alors à nous pour enregistrer des objets.

  • Créer un fichier XML qui contient les variables membres d'un objet. C'est une technique pratique car le fichier qui en résulte est un fichier texte, mais elle est plutôt lourde à mettre en place. Nous n'en traiterons pas dans ce chapitre, mais sachez que c'est une méthode utilisée par un objet : NSDictionary. Vous pouvez d'ailleurs essayer la méthode - writeToFile:atomically: et regarder le fichier produit.

  • Adopter le protocole NSCoding. C'est ce que nous allons faire sur-le-champ.

Le protocole NSCoding

Vous vous souvenez des protocoles ? Un petit rappel rapide : ce sont des listes de méthodes qu'une classe doit implémenter.

Le protocole NSCoding contient deux méthodes : une qui va initialiser un objet à partir d'un fichier (on appelle cela désarchiver un objet), et une autre qui va enregistrer un objet dans un fichier (on dit archiver un objet). L'avantage de réunir ces méthodes dans un protocole est que d'une part, on peut vérifier qui implémente le protocole, et d'autre part, tous les objets l'implémentant sont uniformes : les méthodes sont les mêmes pour tout le monde, et c'est très pratique. :)

Voici le code du protocole :

@protocol NSCoding
- (void) encodeWithCoder:(NSCoder *) encoder;
- (id) initWithCoder:(NSCoder *) decoder;
@end

Il est donc obligatoire pour toute classe voulant se conformer au protocole d'implémenter - initWithCoder: et - encodeWithCoder:.

Qu'est-ce que cet objet NSCoder passé en paramètre ?

NSCoder est une classe abstraite. C'est une classe que l'on ne peut pas instancier (en d'autres termes, on ne peut pas créer d'objet NSCoder). En effet, la classe contient bon nombre de choses très utiles, mais elle n'est pas complète. Si l'on veut créer un objet avec les caractéristiques de la classe NSCoder, il faut instancier une de ses classes filles. Ce sont d'ailleurs des instances des classes filles qui sont passées en paramètre lorsque ces méthodes sont appelées. ;)

Concrètement, cet objet NSCoder va nous aider à archiver ou désarchiver les variables d'un objet.

Prenons un exemple : nous allons archiver une salle de classe dans un fichier. Pour cela, nous allons avoir besoin de trois classes : une classe SalleDeClasse, une classe Professeur et une classe Eleve.

Dans une SalleDeClasse, il y a un professeur et plusieurs élèves. Il y aura donc une variable membre de type Professeur et un tableau (NSArray) qui contiendra les Eleve. ;)

#import <Foundation/Foundation.h>

#import "Professeur.h"
#import "Eleve.h"

@interface SalleDeClasse : NSObject <NSCoding>
{
    Professeur * prof;
    NSArray * eleves;
}

- (id) init;
- (id) initWithCoder:(NSCoder *) decoder;
- (void) encodeWithCoder:(NSCoder *) encoder;
- (void) dealloc;
- (NSString *) description;

@end
#import "SalleDeClasse.h"

@implementation SalleDeClasse

- (id) init
{
    self = [super init];

    if (self) {
        prof = [[Professeur alloc] initAvecNom:@"M. Julien" matiere:@"Latin"];
        Eleve * ludo = [[Eleve alloc] initAvecNom:@"Ludovic" age:12];
        Eleve * marine = [[Eleve alloc] initAvecNom:@"Marine" age:13];
        Eleve * camille = [[Eleve alloc] initAvecNom:@"Camille" age:11];

        eleves = [[NSArray alloc] initWithObjects: ludo, marine, camille, nil];

        /* Pas de problème avec la mémoire ici.
         * Les objets reçoivent - retain une fois dans le tableau. */
        [ludo release];
        [marine release];
        [camille release];
    }
    
    return self;
}

- (void) dealloc
{
    [prof release];
    [eleves release];
    [super dealloc];
}

- (id) initWithCoder:(NSCoder *) decoder
{
    /* On lit l'objet à partir de données */
}
 
- (void) encodeWithCoder:(NSCoder *) encoder
{
    /* On archive l'objet */
}

- (NSString *) description
{
    return [NSString stringWithFormat:@"Professeur : %@\nEleves : %@\n", prof, eleves];
}

@end
#import <Foundation/Foundation.h>

@interface Professeur : NSObject <NSCoding>
{
    NSString * nom, * matiere;
}

- (id) initAvecNom:(NSString *) unNom matiere:(NSString *) uneMatiere;
- (id) initWithCoder:(NSCoder  *) decoder;
- (void) encodeWithCoder:(NSCoder *) encoder;
- (void) dealloc;
- (NSString *) description;

@end
#import "Professeur.h"

@implementation Professeur

- (id) initAvecNom:(NSString *) unNom matiere:(NSString *) uneMatiere
{
    self = [super init];

    if (self) {
        nom = [unNom copy];
        matiere = [uneMatiere copy];
    }

    return self;
}

- (void) dealloc
{
    [nom release];
    [matiere release];
    [super dealloc];
}

- (id) initWithCoder:(NSCoder *) decoder
{
    /* On lit l'objet à partir de données */
}
 
- (void) encodeWithCoder:(NSCoder *) encoder
{
    /* On archive l'objet */
}

- (NSString *) description
{
    return [NSString stringWithFormat:@"%@ enseigne le %@.", nom, matiere];
}

@end
#import <Foundation/Foundation.h>

@interface Eleve : NSObject <NSCoding>
{
    NSString * nom;
    int age;
}

- (id) initAvecNom:(NSString *) unNom age:(int) unAge;
- (id) initWithCoder:(NSCoder *) decoder;
- (void) encodeWithCoder:(NSCoder *) encoder;
- (void) dealloc;
- (NSString *) description;

@end
#import "Eleve.h"

@implementation Eleve

- (id) initAvecNom:(NSString *) unNom age:(int) unAge
{
    self = [super init];

    if (self) {
        nom = [unNom copy];
        age = unAge;
    }

    return self;
}

- (void) dealloc
{
    [nom release];
    [super dealloc];
}

- (id) initWithCoder:(NSCoder *) decoder
{
    /* On lit l'objet à partir de données */
}
 
- (void) encodeWithCoder:(NSCoder *) encoder
{
    /* On archive l'objet */
}

- (NSString *) description
{
    return [NSString stringWithFormat:@"%@ a %d ans", nom, age];
}

@end

Il s'agit d'un code assez simple. Si vous avez des difficultés à le comprendre, je vous conseille de relire la partie I de ce tutoriel.

J'ai surligné dans le code les lignes relatives au protocole NSCoding. On peut donc remarquer que les classes adoptent NSCoding grâce à cette ligne : @interface Eleve : NSObject <NSCoding>. Les autres lignes surlignées sont les méthodes du protocole. J'ai délibérément laissé leur implémentation vide pour qu'on puisse parler de l'archivage des objets pas à pas. ;)

Archiver des objets dans un fichier

Pour commencer, on va archiver l'objet Eleve, c'est-à-dire implémenter la méthode - encodeWithCoder:. C'est l'objet NSCoder qui va nous aider à accomplir cette tâche. En effet, une instance de NSCoder possède plusieurs méthodes dans le but d'archiver des variables. On va donc appeler ces méthodes pour chaque variable de Eleve. Il va nous falloir archiver nom (de type NSString) et age (de type int). Voici les différentes méthodes qui archivent des variables :

<tableau>
<ligne>
<entete>Type</entete>
<entete>Méthode</entete>
</ligne>
<ligne>
<cellule>BOOL</cellule>
<cellule>- encodeBool:forKey:</cellule>
</ligne>
<ligne>
<cellule>int</cellule>
<cellule>- encodeInt:forKey:</cellule>
</ligne>
<ligne>
<cellule>double</cellule>
<cellule>- encodeDouble:forKey:</cellule>
</ligne>
<ligne>
<cellule>float</cellule>
<cellule>- encodeFloat:forKey:</cellule>
</ligne>
<ligne>
<cellule>id</cellule>
<cellule>- encodeObject:forKey:</cellule>
</ligne>
</tableau>

Ces fonctions attendent deux paramètres... À quoi sert le deuxième ?

Ce deuxième paramètre est une chaîne de caractères portant le nom de la variable que vous souhaitez archiver. Ainsi, si on veut coder age, on donnera en paramètre @"age". Cela sert à repérer la variable dans le fichier.

Dans l'exemple ci-dessous, nous avons deux variables à archiver : une de type id, l'autre de type int. On va donc écrire la méthode - encodeWithCoder: comme suit :

- (void) encodeWithCoder:(NSCoder *) encoder
{
    [encoder encodeObject:nom forKey:@"nom"];
    [encoder encodeInt:age forKey:@"age"];
}

Facile, non ? :D Mais là, on a un petit problème. Ici, notre classe Eleve est très simple et hérite de NSObject. Que se passe-t-il si elle hérite d'une autre classe plus complexe ? Les variables membres de celle-ci ne seront pas archivées. Il faut donc appeler la méthode de la classe mère avec super :

- (void) encodeWithCoder:(NSCoder *) encoder
{
    [super encodeWithCoder:encoder];
    [encoder encodeObject:nom forKey:@"nom"];
    [encoder encodeInt:age forKey:@"age"];
}

Et voilà ! On peut à présent faire pareil pour les autres classes. Professeur possède deux variables membres de type id, tout comme SalleDeClasse (qui possède un NSArray et un Professeur).

- (void) encodeWithCoder:(NSCoder *) encoder
{
    [encoder encodeObject:nom forKey:@"nom"];
    [encoder encodeObject:matiere forKey:@"matiere"];
}
- (void) encodeWithCoder:(NSCoder *) encoder
{
    [encoder encodeObject:prof forKey:@"prof"];
    [encoder encodeObject:eleves forKey:@"eleves"];
}

C'est là que l'intérêt du protocole se révèle. L'objet NSCoder ne sait pas comment archiver un objet Professeur (puisque nous l'avons créé nous-mêmes), ni un objet NSArray ! o_O Mais puisque ces objets sont conformes au protocole NSCoding, il peut appeler les méthodes nécessaires en toute confiance.

C'est ce qui se passe pour archiver le NSArray contenant les Eleve. Lorsque celui-ci va recevoir l'ordre de s'archiver, il va appeler les méthodes - encodeWithCoder: de chaque Eleve, qui vont eux-mêmes appeler cette méthode pour archiver leur nom, qui est aussi un objet. C'est très pratique, car chacun s'occupe de ses petites affaires mais, au final, tout est propre archivé. :)

Mais on ne possède aucune information sur le nom du fichier ! On ne sait pas où on écrit !

En effet, il manque une étape avant de pouvoir archiver notre SalleDeClasse. On a pour cela besoin de la classe NSKeyedArchiver, qui est une classe fille de NSCoder. ;)

On va à présent se servir d'une méthode de classe dont voici le prototype :

+ (BOOL)archiveRootObject:(id)rootObject toFile:(NSString *)path

Elle prend en paramètre l'objet que l'on veut archiver et le chemin, sous la forme d'une chaîne de caractères, du fichier dans lequel écrire. Elle retourne YES si tout s'est bien passé ou NO en cas de problème.

Profitons-en pour archiver notre objet SalleDeClasse :

#import <Foundation/Foundation.h>
#import <stdlib.h>

#import "SalleDeClasse.h"

int main (void)
{
    NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
    SalleDeClasse * salle40 = [[SalleDeClasse alloc] init];

    NSLog(@"%@", salle40); // On affiche la description de la salle.

    /* On archive l'objet dans un fichier */
    if (![NSKeyedArchiver archiveRootObject:salle40 toFile:@"salle40.classe"]) {
        NSLog(@"Une erreur s'est produite !");
        [salle40 release];
        return EXIT_FAILURE;
    }
    NSLog (@"Écriture réussie !");

    [salle40 release];
    [pool drain];
    return EXIT_SUCCESS;
}

Voici la sortie du programme à l'exécution :

[Infos log] Professeur : M. Julien enseigne le Latin.
Eleves : (
    Ludovic a 12 ans,
    Marine a 13 ans,
    Camille a 11 ans
)
[Infos log] Écriture réussie !

Notre salle de classe est à présent archivée dans salle40.classe ! :D

Désarchiver des objets

Il nous faut à présent implémenter la deuxième méthode du protocole NSCoding, à savoir - initWithCoder:. Le principe de cette méthode est quasiment le même que celui de la méthode utilisée pour archiver un objet. En effet, on va appeler plusieurs méthodes de NSCoder qui vont chacune désarchiver les variables membres :

<tableau>
<ligne>
<entete>Type</entete>
<entete>Méthode</entete>
</ligne>
<ligne>
<cellule>BOOL</cellule>
<cellule>- (BOOL)decodeBoolForKey:</cellule>
</ligne>
<ligne>
<cellule>int</cellule>
<cellule>- (int)decodeIntForKey:</cellule>
</ligne>
<ligne>
<cellule>double</cellule>
<cellule>- (double)decodeDoubleForKey:</cellule>
</ligne>
<ligne>
<cellule>float</cellule>
<cellule>- (float)decodeFloatForKey:</cellule>
</ligne>
<ligne>
<cellule>id</cellule>
<cellule>- (id)decodeObjectForKey:</cellule>
</ligne>
</tableau>

Comme vous le voyez, ces méthodes retournent la variable qu'elles ont désarchivée. Dans le cas d'un objet, celui-ci ne vous appartient pas, vous n'avez donc pas à gérer la mémoire. Le paramètre est la chaîne de caractères utilisée à l'archivage, c'est-à-dire le nom de la variable. Implémentons la méthode - initWithCoder: pour la classe Eleve :

- (id) initWithCoder:(NSCoder *) decoder
{
    self = [super init];

    if (self) {
        nom = [[decoder decodeObjectForKey:@"nom"] retain];
        age = [decoder decodeIntForKey:@"age"];
    }

    return self;
}

Pourquoi appelle-t-on - retain pour le nom ?

Si vous observez la méthode - dealloc de Eleve, elle envoie le message - release à la variable membre nom. Donc, pour rester conforme à la règle d'or, je retiens l'objet. Par convention, je préfère être propriétaire des variables membres. ;)

Remarquez que, dans ce code, on envoie le message [super init] à la classe mère. En effet, comme tout à l'heure, NSObject n'implémente pas le protocole requis. Si cela avait été le cas, on aurait envoyé le message [super initWithCoder:decoder].

Allez, on se motive et on écrit les méthodes pour les classes Professeur et SalleDeClasse. :p

- (id) initWithCoder:(NSCoder *) decoder
{
    self = [super init];

    if (self) {
        nom = [[decoder decodeObjectForKey:@"nom"] retain];
        matiere = [[decoder decodeObjectForKey:@"matiere"] retain];
    }

    return self;
}
- (id) initWithCoder:(NSCoder *) decoder
{
    self = [super init];

    if (self) {
        prof = [[decoder decodeObjectForKey:@"prof"] retain];
        eleves = [[decoder decodeObjectForKey:@"eleves"] retain];
    }

    return self;
}

Grâce au protocole, on a le même schéma que tout à l'heure. Lors du désarchivage, chaque objet s'occupe de ses variables et, à la fin, tout est désarchivé !

De même que pour l'archivage, il nous faut un objet pour gérer tout cela : j'ai nommé le NSKeyedUnarchiver ! Il remplit exactement la même fonction que NSKeyedArchiver, mais désarchive au lieu d'archiver. On utilise alors la méthode de classe dont voici le prototype :

+ (id) unarchiveObjectWithFile:(NSString *) path

Elle attend en paramètre le chemin de l'archive (dans notre cas, ce sera le chemin de salle40.classe). Allons-y !

#import <Foundation/Foundation.h>
#import <stdlib.h>

#import "SalleDeClasse.h"

int main (void)
{
    NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
    SalleDeClasse * salle = [NSKeyedUnarchiver unarchiveObjectWithFile:@"salle40.classe"];

    NSLog(@"%@", salle); // On affiche la description de la salle.

    return EXIT_SUCCESS;
}

Ce qui donne bien l'output suivant :

Professeur : M. Julien enseigne le Latin.
Eleves : (
    Ludovic a 12 ans,
    Marine a 13 ans,
    Camille a 11 ans
)

Miracle, on a réussi. On retrouve bien notre salle40 ! :)

Enfin ! C'était long et difficile, mais essentiel si vous souhaitez vous lancer par la suite dans l'utilisation de Cocoa pour concevoir des applications graphiques.

Ce qu'il faut retenir de ce chapitre, c'est la technique d'implémentation du protocole NSCoding. C'est vraiment important pour Cocoa, car un bon nombre de classes l'utilisent. Il faut également retenir la méthode « passe-partout » très pratique de lecture et d'écriture avec la classe NSString.

Le prochain chapitre est un TP ! Nous mettrons en application toutes les connaissances acquises jusqu'à maintenant. :)

Liens vers la doc

Cette partie est encore en chantier. Le prochain chapitre sera un TP qui mettra en pratique toutes les connaissances acquises jusqu'à maintenant. ;)

Ensuite viendront sûrement des parties sur les classes NSObject et NSData, ainsi que d'autres bonus.

Example of certificate of achievement
Example of certificate of achievement