Sauvegarde Plist
Adam
Membre
Bonjour,
J'entame à présent ma seconde application, et j'ai un petit soucis.
J'ai une classe Personne avec différents attributs (ex: Personne.nom, Personne.prénom...).
Chaque personne est ajouté dans un tableau :
Par la suite, je veux pouvoir sotcker ces "Personnes" dans un fichier Plist. Je crée donc un nouveau dictionnaire, dans lequel j'ajoute mon tableauUtilisateur.
Le problème c'est que le dictionnaire n'est pas sauvegardé dans la Plist.
Pourtant le code me semble correcte :
Je pense que j'ai fait une erreur par rapport à mon tableauUtilisateur, non ? On peut stocker des entités dans une Plist ?
Quand je dis entité, je parle d'une "Personne" avec ses attributs qui est sous la forme :"<Personne: 0x4e2b750>"
Merci de votre aide.
J'entame à présent ma seconde application, et j'ai un petit soucis.
J'ai une classe Personne avec différents attributs (ex: Personne.nom, Personne.prénom...).
Chaque personne est ajouté dans un tableau :
<br /> [tableauUtilisateur addObject:Personne1]; <br />
Par la suite, je veux pouvoir sotcker ces "Personnes" dans un fichier Plist. Je crée donc un nouveau dictionnaire, dans lequel j'ajoute mon tableauUtilisateur.
Le problème c'est que le dictionnaire n'est pas sauvegardé dans la Plist.
Pourtant le code me semble correcte :
<br /> //Sauvegarde des données<br /> NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);<br /> NSString *documentsDirectory = [paths objectAtIndex:0];<br /> NSString *plistPath = [documentsDirectory stringByAppendingPathComponent:@"preferenceUtilisateur.plist"];<br /> NSString *errorDesc;<br /> NSMutableDictionary *prefDict;<br /> <br /> <br /> //On remplace par les valeur que l'on a modifiées<br /> prefDict=[[NSMutableDictionary alloc] initWithObjects:tableauUtilisateur forKeys:[NSArray arrayWithObject:@"utilisateur"]];<br /> [prefDict writeToFile:plistPath atomically:YES];<br /> NSData *plistData = [NSPropertyListSerialization dataFromPropertyList:prefDict<br /> format:NSPropertyListXMLFormat_v1_0<br /> errorDescription:&errorDesc];<br /> <br /> <br /> if (plistData) {<br /> [plistData writeToFile:plistPath atomically:YES];<br /> }<br /> else {<br /> [errorDesc release];<br /> }<br /><br /><br /> [prefDict release];<br />
Je pense que j'ai fait une erreur par rapport à mon tableauUtilisateur, non ? On peut stocker des entités dans une Plist ?
Quand je dis entité, je parle d'une "Personne" avec ses attributs qui est sous la forme :"<Personne: 0x4e2b750>"
Merci de votre aide.
Connectez-vous ou Inscrivez-vous pour répondre.
Réponses
si mes souvenirs sont bon, il faut que ta classe Personne implémente le protocole NSCoding. De plus il me semble que les seul éléments que tu puisse sauver dans une plist sont: les string (NSString), les tableaux (NSArray) et les dictionnaires (NSDictionnary); j'ai un doutes sur les data (NSData).
C'est donc assez contraignant. Tu pourrais avantageusement remplacer cette solution par l'utilisation de la classe NSKeyedArchiver ou même de CoreData (qui lui fera presque tout tout seul). Sauf évidemment si tu veux que ton fichier soit "humainement" lisible...
Comment ça je dois l'implémenter avec NScoding ? J'ai lu la doc, mais ya quelques points qui m'échappe.
J'essaye de l'intancier, mais je comprend pas trop comment ça fonctionne :
Je comprends pas comment l'implementer.
Et une fois que j'ai ces 2 méthodes, j'utilise data pour les stocker, c'est bien ça ?
Si tu mets des breakpoints dans ton code, que tu regarde que vaut le paramètre error en retour de la méthode d'écriture du plist, etc, bref que tu débugues un peu pas à pas, ça donne quoi ?
Sinon il se trouve que ton code écrit 2 fois ton dictionnaires, dans le plist, en utilisant 2 façons de faire différentes... sauf que toi tu utilises les deux : à la fois la méthode "writeToFile:atomically:" de NSDictionary qui te permet en effet de sérialiser un NSDictionary dans un fichier PLIST, et la méthode de NSPropertyListSerialization qui permet de faire une sérialisation d'un objet dans un PLIST également... mais de façon plus fine en contrôlant les paramètres du style format du PLIST, etc.
Ces deux méthodes sont à peu près équivalentes (l'une permettant de préciser plus de paramètres que l'autre mais bon).
Par contre, quelle que soit la méthode utilisée, il n'est possible de sérialiser un NSDictionary (tout comme pour un NSArray) |b]que si ce dernier ne contient que des PLIST-Objects[/b] (NSData, NSNumber, NSString, NSDate, NSDictionary, NSArray), que donc Cocoa saura tout seul transformer en leur représentation XML dans le PLIST.
Or dans ton cas, tu as dans ton NSDictionary ton tableau [tt]tableauUtilisateur[/tt] qui contient... des objets de la classe "Personne" ! Et ça du coup c'est pas possible pour Cocoa de savoir comment les représenter en XML, dans un PLIST.
Donc dans ce cas de figure, seulement 2 solutions possibles :
1) Prévoir une représentation intermédiaire de tes objets personne de sorte que cette représentation ne contienne que des PLIST-Objects (par exemple construire, pour chaque objet Personne, un NSDictionary contenant les clés nom, prénom, etc... bref les attributs de ton objet que tu veux écrire dans le plist). Ca t'oblige à boucler sur chaque Personne de ton tableau le temps de créer un tableau intermédiaire non pas d'objet "Personne" mais de "NSDictionary représentant ces même Personnes", que lui tu pourras stocker dans un PLIST. Et à la lecture du PLIST, faire l'opération inverse, reconstruire un objet Personne à partir du NSDictionary récupéré du PLIST. Un peu fastidieux.
2) Ou bien à ce stade en effet, autant passer par des NSCoders, donc en faisant en sorte que ta classe Personne implémente le protocole NSCoding (tu as des exemples dans la doc dans le Programming Guide associé). Et dans ce cas quand tu vas sérialiser ton tableau de Personnes, tu vas finir par créer une "archive", qui est un fichier binaire (dans un format interne à Cocoa dont j'ai jamais cherché à comprendre l'organisation, pas la peine) qui contiendra tous tes objets "Personne" sérialisés. Et pour la relecture, tu utilises aussi le mécanisme inverse et tu vas directement récupérer ton NSArray d'objets "Personne".
Avantage tu peux stocker n'importe quel type d'objet du moment que tu implémentes NSCoding. Inconvénient le fichier de sauvegarde ne va plus être dans un format tres compréhensible et ne sera pas éditable avec un outil externe comme Property List Editor, du moins pas facilement vu la tronche du format. Mais après tout est-ce un problème, du moment que ton logiciel, lui, arrive à écrire et lire cette archive...
J'aimerais utiliser la solution utilisant la classe NSCoding.
La 1ere solution qui consiste de créer un dictionnaire à chaque utilisateur, c'est ce que j'ai fait au début. Mais comme c'est vraiment pas pratique, j'ai voulu m'orienter vers quelque chose de plus efficace.
Ah ouai, je faisais fausse piste, j'avais mis le protocle NSCoding dans ma Viewcontroller actuelle et non dans ma classe Personne.
Donc maintenant j'ai modifié ma classe Personne afin qu'elle corresponde à NScoding :
Personne.h :
Personne.m :
Concernant ma classe Personne, normalement c'est OK, non ?
Ensuite je vais dans ma ViewController ou je dois sauvegarder mon tableau d'utilisateur, et je fais donc une Archive que j'encode avec mon tableau :
Mais le resultat n'a pas l'air de fonctionner, sa me donne :
"$archiver" = NSKeyedArchiver;
"$objects" = (
"$null",
{
"$class" = "<CFKeyedArchiverUID 0x4b51f30 [0xe54400]>{value = 7}";
"NS.objects" = (
"<CFKeyedArchiverUID 0x4b51c80 [0xe54400]>{value = 2}"
);
},
{
"$class" = "<CFKeyedArchiverUID 0x4b52110 [0xe54400]>{value = 6}";
identifiant = "<CFKeyedArchiverUID 0x4b51ff0 [0xe54400]>{value = 4}";
motDePasse = "<CFKeyedArchiverUID 0x4b52100 [0xe54400]>{value = 5}";
nom = "<CFKeyedArchiverUID 0x4b520f0 [0xe54400]>{value = 0}";
prenom = "<CFKeyedArchiverUID 0x4b520e0 [0xe54400]>{value = 3}";
},
gui,
vvvv,
fvs,
{
"$classes" = (
Utilisateur,
NSObject
);
"$classname" = Utilisateur;
},
{
"$classes" = (
NSMutableArray,
NSArray,
NSObject
);
"$classname" = NSMutableArray;
}
);
"$top" = {
utilisateur = "<CFKeyedArchiverUID 0x4b51dd0 [0xe54400]>{value = 1}";
};
"$version" = 100000;
}
Comme son nom l'indique, decodeObjectForKey renvoie un objet autoreleasé. Il ne faut pas oublier de le retenir:
Au premier coup d'oe“il, l'enregistrement me parait bon. Le format des PList utilisé par la sérialisation n'est pas fait pour être lisible par un être humain. C'est lié au fait qu'un graph d'objet n'est pas forcément organisé de façon hiérarchique (contrairement au format XML).
Si tu veux un PList lisible, il vaut mieux utiliser la méthode évoquée par Ali, avec laquelle tu auras plus de contrôle.
Mais pour le charger alors, vu qu'à la base j'ai une archive. J'ai un problème pour récupérer mon tableau de ma nouvelle archive :
Si je fait un NSLog de tableauUtilisateur, avant la sauvegarde et apres la recharge, j'ai 2 valeurs différentes (ce qui fait bugger ma tableView) :
2011-05-31 16:42:16.699 AdeUhp[7442:207] Au début on a (avant sauvegarde): (
"<Utilisateur: 0x4b3bb20>"
)
2011-05-31 16:42:18.818 AdeUhp[7442:207] Apres la sauvegarde : (
"<Utilisateur: 0x4b4e420>"
)
Du coup, pour répondre à ta question sur ton décodage, bien sûr que non ce n'est pas normal que tu n'utilises pas plistPath. Le principe comme je viens de le dire est de relire le contenu du fichier dans un NSData, puis d'utiliser un NSUnarchiver pour décoder lesdites data lues.
Là ton code initialise "data "avec un simple "alloc"+"init", donc tu crées un NSData vide... et ensuite tu crées un NSUnarchiver pour décoder... ce NSData vide. Forcément, en sortie tu ne risques pas d'obtenir grand chose.
C'est un NSData contenant les octets du fichier à lire qu'il faut décoder, pas un NSData vide. Donc quand tu crées ton NSData* data il faut l'initialiser avec le contenu du fichier ([tt]initWithContentsOfFile:[/tt] bien sûr.
Et après comme te l'as indiqué Céroce, quand tu demandes à un NSUnarchiver/NSCoder "decodeObjectForKey:", ne pas oublier de faire un "retain" sur le résultat, puisqu'il te retourne un objet autoreleased. (et bien sûr de balancer ce "retain" par un "release" dans le destructeur/dealloc de ta classe). Ou encore mieux, d'utiliser une [tt]@property(retain) NSArray* tableauPersonnes;[/tt] et la notation [tt]self.tableauPersonnes[/tt] pour manipuler ta propriété, plutôt que de manipuler une variable d'instance, comme ça ta propriété fait toute seule les retain/release à chaque affectation.
ça donne quelque chose comme ça:
C'est pourquoi je me suis permis d'éditer.
Merci Ceroce pour ce code, mais j'ai trouvé quelque chose qui semble bien fonctionner.
Donc maintenant pour charger j'utilise cette méthode qui fonctionne (je retrouve bien le bon utilisateur) :
Mais le problème si je clique sur une de mes cellules, j'ai un EXC_BAD, qui vient de plusieurs endroits (ça dépend des fois). Mais ça met toujours en erreur mon tableauUtilisateur...
D'après ce que j'ai compris, ça pourrait être à cause de retain, c'est ça ? pourtant je met juste le resultat dans mon tableauUtilisateur (qui est alloué au début dans le ViewDidLoad, et relâché à la fin dans le dealloc), ce n'est pas bon ?
Merci beaucoup pour votre aide.
C'est un point crucial et incontournable, et si tu commences à coder sans en avoir compris les principes et sans que ça devienne naturel tu vas droit dans le mur.
Mieux vaut prendre un peu de temps pour comprendre les règles de gestion mémoire et après ne plus avoir de problème, que de coder sans trop les avoir comprises et faire des applications qui vont soit avoir des fuites mémoires et gonfler à vue d'oeil, soit planter sans prévenir.
Et à ton code ça se voit qu'il te manque quelques éléments de base sur le sujet, car par exemple faire un removeAllObjects sur un tableau qu'au final tu ne vas plus jamais réutiliser après, puisque dans cette variable d'instance tableauPersonnes à la ligne d'après tu remplaces l'objet précédent par un tout nouveau (celui que te rend decodeObjectForKey) donc l'objet qui était avant dans tableauPersonne va être perdu dans la nature et provoquer une jolie fuite mémoire dans ton application...
Et sinon, ton code n'est pas conformer à ce qui doit être fait pour gérer le rootObject de ton graphe d'objets que tu sérialises (en l'occurrence ton tableau de personnes). Reprend le code qu'a indiqué Céroce, car là ton code s'il marche de prime abord car tu n'as sans doute pas de référence cyclique entre tes objets ni cas de boucles (genre personne1.mari = personne2 et personne2.femme = personne1 ==> boucle) donc t'as de la chance que ça déconne pas, mais c'est pas un pari à faire pour avoir une appli stable ! En plus ton graphe d'objets Personnes que tu stockes n'a pas de racine officiellement définie si tu utilises un code comme le tien... Bref faut vraiment utiliser la méthode pour manipuler le rootObject, celle qu'a mis Céroce quoi.
Oui concernant la mémoire, c'est ce que je fais (mais j'ai toujours du mal à comprendre)..
Okay donc la méthode de "Ceroce" est beaucoup mieux que la mienne, je vais la reprendre.
Merci beaucoup de votre aide, c'est rare les forums comme ça.
J'ai bien suivi la méthode de "Céroce" pour sauvegarder et charger mes données.
Mais à la première utilisation de mon application, il n'y a pas encore de données inscrite et je fait un "load" dans la Plist.
Il y a donc rien inscrit dans la Plist, et je load, ce qui me renvoi un NSLog : Error while loading personnes: The operation couldn't be completed. (Cocoa error 260.)
Mais je ne peux pas enlever ce load, car une fois l'application a sauvegardé dedans, tout est OK.
Est-ce que cela peut poser problème ?
Merci
Ouai mais si il charge et qu'il y a rien dans le fichier, ça peut créer des problèmes ?
C'est lors du chargement à cet endroit :
Mais ça le fait que à la 1ere utilisation du programme. C'est à dire quand il y a une TableView vide, aucun contact n'est encore présent. Mais un fois un contact créé, ce message n'apparait plus.