New tuto 8 : Parser du XML et du JSON !

Lorsque vous utiliserez des webservices, vous aurez très certainement besoin de parser des fichiers, qu’ils soient XML ou JSON. Ce tutoriel vous apprend à le faire .

Utiliser XML

Parsons un fichier XML contenant des pays. Créez un nouveau projet Window-based Application que vous nommerez XML. Écrivez ces quelques lignes dans un fichier .xml (capitales.xml) et ajoutez-le au projet (rappel : glisser-déposer le fichier directement dans Xcode).

<?xml version=“1.0“ encoding=“UTF-8“?>
<Monde>
<pays>
<nom>France</nom>
<capitale>Paris</capitale>
<habitants>60M</habitants>
</pays>
<pays>
<nom>Suisse</nom>
<capitale>Berne</capitale>
<habitants>7,5M</habitants>
</pays>
<pays>
<nom>Allemagne</nom>
<capitale>Berlin</capitale>
<habitants>82M</habitants>
</pays>
</Monde>

Il existe un parser XML intégré directement dans le framework Foundation : NSXMLParser. Nous allons donc nous en servir. Pour cela, créez une nouvelle classe, subclass of NSObject que vous nommerez XMLParser.
Implémentons cette classe en commençant par la méthode qui lancera le parsage :

- (void)parseXMLFileAtPath:(NSString *)path {
stories = [[NSMutableArray alloc] init]; // 1
 
NSURL *xmlURL = [NSURL fileURLWithPath:path];
 
textParser = [[NSXMLParser alloc] initWithContentsOfURL:xmlURL];
 
[textParser setDelegate:self]; // 2
 
[textParser parse]; // 3
}

Chaque pays présent dans le fichier XML va être ajouté dans un tableau : countries, que l’on initialise ligne 1.
Ligne 2, la classe NSXMLParser propose un protocole delegate, et on choisit de l’implémenter. C’est obligatoire pour que le parser soit opérationnel. Enfin, on lance le parsage ligne 3.
Regardons maintenant le .h où l’on déclare tous les objets nécessaires pour construire le tableau des pays.

#import <Foundation/Foundation.h>
 
#define kPays @"pays"
#define kCapitale @"capitale"
#define kNom @"nom"
#define kPopulation @"habitants"
 
@protocol XMLParserDelegate;
 
 
@interface XMLParser : NSObject <NSXMLParserDelegate> {
 
NSXMLParser *textParser;
NSMutableArray *stories;
 
 
// a temporary item; added to the "stories" array one at a time, and cleared for the next one
// un objet temporaire (ici un pays).
// il est ajouté au tableau "stories" puis effacé pour le nouveau pays
NSMutableDictionary *item;
 
// On va parser le document de haut en bas.
// On va donc collecter chaque sous-élements, les sauver dans item, puis ajouter dans le tableau
 
NSString *currentElement;
NSMutableString *currentCountry;
NSMutableString *currentPopulation;
NSMutableString *currentCapital;
 
id<XMLParserDelegate> delegate;
 
}
 
- (void)parseXMLFileAtPath:(NSString *)path;
 
@property (nonatomic, retain) NSMutableArray *stories;
@property (nonatomic, assign) id<XMLParserDelegate> delegate;
 
@end
 
@protocol XMLParserDelegate <NSObject>
- (void) xmlParserdidFinishParsing;
@end

N’oubliez pas ces lignes dans le .m :

#import "XMLParser.h"
 
@implementation XMLParser
 
@synthesize stories;
@synthesize delegate;

Ensuite, implémentons les méthodes du delegate. Commençons par celle appelée lorsque le parser rencontre un nouvel élément :

- (void)parser:(NSXMLParser *)parser didStartElement:(NSString *)elementName namespaceURI:(NSString*)namespaceURI qualifiedName:(NSString *)qName attributes:(NSDictionary *)attributeDict{
if(currentElement)
{
[currentElement release];
currentElement = nil;
}
currentElement = [elementName copy]; // 1
 
if ([elementName isEqualToString:kPays]) { // 2
//On efface notre cache qui est item
if(item)
{
  • ;
    item = nil;
    }
    if(currentCountry)
    {
    [currentCountry release];
    currentCountry = nil;
    }
    if(currentCapital)
    {
    [currentCapital release];
    currentCapital = nil;
    }
    if(currentPopulation)
    {
    [currentPopulation release];
    currentPopulation = nil;
    }
    item = [[NSMutableDictionary alloc] init];
    currentPopulation = [[NSMutableString alloc] init];
    currentCapital = [[NSMutableString alloc] init];
    currentCountry = [[NSMutableString alloc] init];
     
    }
    }

    Dans cette méthode, on met à jour l’élément courant rencontré ligne 1. Ensuite, si cet élément (la balise rencontrée) est égale à Pays ligne 2, on sait que l’on rencontre un nouveau pays. On remet donc à zéro tous les objets qui servent à construire item (un dictionnaire contenant les données du pays).
    Après avoir détecté la balise Pays, il faut “mettre en cache” les valeurs des balises Nom, Capitale et Population. Pour cela, implémentez la méthode suivante :

    - (void)parser:(NSXMLParser *)parser foundCharacters:(NSString *)string{
    // on sauve les éléments du pays pour l'item en cours
    if ([currentElement isEqualToString:kCapitale])
    [currentCapital appendString:string];
    else if ([currentElement isEqualToString:kNom])
    [currentCountry appendString:string];
    else if ([currentElement isEqualToString:kPopulation])
    [currentPopulation appendString:string];
    }

    Ensuite, définissons la méthode appelée lorsque l’on rencontre une balise de fermeture (exemple : </ balise>). Dans cette méthode, lorsque l’on ferme pays, on sauve les valeurs des balises nom, capitale et population pour le pays en question, puis on ajoute ce pays dans le tableau des pays.

    - (void)parser:(NSXMLParser *)parser didEndElement:(NSString *)elementName namespaceURI:(NSString*)namespaceURI qualifiedName:(NSString *)qName{
    if ([elementName isEqualToString:kPays]) {
    // on sauve les valeurs dans item, qui est ensuite ajouté dans le tableau stories
  • ;
  • ;
  • ;
     
    [stories addObject:item];
    }   
    }

    Enfin, lorsque le parser a fini de parcourir le document, on choisit d’avertir le delegate. En effet, nous allons présenter les données parsées grâce à une table view. Cette méthode est appelée sur le thread principal, car nous allons par la suite parser dans un thread détaché pour l’occasion.

    - (void)parserDidEndDocument:(NSXMLParser *)parser {
     
    NSLog(@"C'est fini !");
    NSLog(@"stories a %d pays", [stories count]);
    [self performSelectorOnMainThread:@selector(tellTheDelegateItIsFinished) withObject:nilwaitUntilDone:NO];
    }
     
    - (void) tellTheDelegateItIsFinished {
    [delegate xmlParserdidFinishParsing];  
    }

    Et le dealloc alors ? tongue

    - (void) dealloc {
    [stories release];
    [textParser release];
  • ;
    [currentCapital release];
    [currentCountry release];
    [currentPopulation release];
     
    [super dealloc];
     
    }

    Créons maintenant le table view controller qui gérera l’affichage des données du fichier XML. Pour cela, ajoutez un nouveau fichier UIViewController, de type UITableViewController, que vous nommerez XMLTableViewController. Dans l’application delegate, ajoutez ces lignes :
    Dans le .h

    #import <UIKit/UIKit.h>
    #import "XMLTableViewController.h"
     
    @interface XMLAppDelegate : NSObject <UIApplicationDelegate> {
     
    XMLTableViewController *tableViewController;
     
    }
     
    @property (nonatomic, retain) IBOutlet UIWindow *window;
     
    @end

    Dans le .m

    - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary*)launchOptions
    {
    // Override point for customization after application launch.
    tableViewController = [[XMLTableViewController alloc] initWithStyle:UITableViewStylePlain];
    [self.window addSubview:tableViewController.view];
     
    [self.window makeKeyAndVisible];
    return YES;
    }
     
    - (void)dealloc
    {
    [tableViewController release];
    [_window release];
    [super dealloc];
    }

    Ensuite, modifiez ces méthodes dans le table view controller fraîchement créé :

    - (void)viewDidLoad
    {
    [super viewDidLoad];
     
    self.clearsSelectionOnViewWillAppear = NO;
    NSString *xmlFilePath =[[NSBundle mainBundle] pathForResource:@"capitales" ofType:@"xml"]; // 1
     
    [self performSelectorInBackground:@selector(parseXMLFile:) withObject:xmlFilePath];
    }
     
    - (void)parseXMLFile:(NSString*)thePath {
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; // 2
     
    parser = [[XMLParser alloc] init];
    parser.delegate = self// 3
     
    if ([parser.stories count] == 0)
    {
    [parser parseXMLFileAtPath:thePath];
    }
     
    [pool release];
    }

    Ligne 1, on choisit donc de parser le document dans un thread séparé.Ainsi, le thread principal n’est pas dédié au parsage, et reste libre pour intercepter et réagir aux événements utilisateurs, pour afficher des éléments à l’écran.
    En détachant un thread, on doit créer un bassin d’autorelease, ce que l’on fait ligne 2. En effet, le bassin d’autorelease créé dans le fichier main.m n’est disponible que pour le thread principal. Pour permettre au parser d’appeler le rafraîchissement de l’affichage des données, on set la propriété delegate ligne 3.
    Enfin, pour afficher les données :

    #pragma mark - XMLParser delegate
    - (void) xmlParserdidFinishParsing {
    [self.tableView reloadData];
    }
     
    #pragma mark - Table view data source
     
    - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
    {
    // Return the number of sections.
    return 1;
    }
     
    - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
    {
    // Return the number of rows in the section.
    return [parser.stories count];
    }
     
    - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
    {
    static NSString *CellIdentifier = @"Cell";
     
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
    if (cell == nil) {
    cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease];
    }
     
    NSDictionary *pays = [parser.stories objectAtIndex:[indexPath row]];
    cell.textLabel.text = [NSString stringWithFormat:@"%@ — %@ — %@", [pays objectForKey:kNom], [pays objectForKey:kCapitale], [pays objectForKey:kPopulation]];
     
    return cell;
    }

    N’oubliez pas le dealloc !

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

    Pour information, voici le .h :

    #import <UIKit/UIKit.h>
    #import "XMLParser.h"
     
    @interface XMLTableViewController : UITableViewController <XMLParserDelegate> {
    XMLParser *parser;
    }
     
    @end

    Compilez et lancez votre application, vous devriez obtenir une liste comme ceci :

    Pour parser un fichier XML directement depuis une source Internet, il suffit de créer un objet NSURL du fichier XML et le passer directement en paramètre de la méthode :

    textParser = [[NSXMLParser alloc] initWithContentsOfURL:xmlURL];

    Utiliser le JSON

    L’iPhone et l’iPad sont des appareils mobiles et connectés qui permettent un accès facile à Internet. Bien que l’on puisse utiliser le navigateur intégré, grâce à une UIWebView, il est bien souvent plus confortable pour l’utilisateur de lancer une application spécifique à l’iPhone, dite optimisée, pour accéder aux informations qui l’intéressent, comme sa page Facebook ou son fil Twitter.
    Cet accès est possible grâce aux webservices qui sont en quelque sorte des sites web spécifiques, sans mise en page, qui nous permettent d’obtenir les informations nécessaires. Beaucoup de sites proposent des API qui utilisent le JSON comme système de fichier en plus du traditionnel XML.

    Voici quelques exemples :
    • Twitter.http://apiwiki.twitter.com/Twitter-Search-API-Method%3A-trends-current
    • Facebook. http://wiki.developers.facebook.com/ind … mments.get
    • Météo. http://www.geonames.org/export/JSON-webservices.html

    Pourquoi le JSON ?

    La question n’est pas anodine. Le JSON a séduit les développeurs par sa simplicité et sa rapidité. Même si ce n’est pas une technologie mise en avant par Apple, qui utilise du XML pour les plist par exemple, le JSON a l’avantage d’être un format plus compact et donc plus rapide lors de son téléchargement sur un webservice.
    Comparaison sur un tableau de score par exemple :

    XML :

    <?xml version="1.0" encoding="UTF-8"?>
    <Resultats>
    <Joueur>
    <Nom>Jean Martin</Nom>
    <Score>10000</Score>
    </Joueur>
    <Joueur>
    <Nom>Pierre Dupond</Nom>
    <Score>9000</Score>
    </Joueur>
    <Joueur>
    <Nom>Alice Bateau</Nom>
    <Score>8500</Score>
    </Joueur>
    </Resultats>

    Longueur : 271 caractères.
    JSON :

    {
    "resultats":{
    "joueurs":[
    {
    "nom":"Jean Martin",
    "score":10000
    },
    {
    "nom":"Pierre Dupond",
    "score":"9000"
    },
    {
    "nom":"Alice Bateau",
    "score":"8500"
    }
    ]
    }
    }

    Longueur : 194 caractères

    Que peut-on conclure de cette comparaison ? Le XML, de par sa taille, va être environ 50 % plus lent au téléchargement pour la même information, ce qui est loin d’être négligeable sur un objet mobile.

    Utilisation sur iPhone

    La méthode présentée n’est pas LA méthode mais elle a le mérite de fonctionner parfaitement. Pour corser le tout, nous allons faire une application compatible iPhone et iPad (Universelle).
    Lancer Xcode et faire : File > New > New Project > Window-based Application et choisir Universal dans Device Family. Appelons ce projet JSON_Tuto.

    Nous allons devoir utiliser un framework qui ne fait pas parti du SDK officiel afin de traiter les fichiers en JSON.
    Rappelez-vous, je vous ai dit qu’Apple fonctionne avec XML pour le moment !

    Il existe plusieurs frameworks pour traiter le JSON sur iPhone, notre préférence va à celui de dispo- nible à cette adresse : http://stig.github.com/json-framework/.
    Il a l’avantage d’avoir fait ses preuves dans beaucoup d’applications.
    Téléchargez la dernière version, vous devriez arriver sur la fenêtre représentée à la figure suivante.
    Pour vous en servir, le plus simple est de glisser directement le contenu du dossier Classes dans votre projet Xcode (et le copier).

       Ca devrait ressembler à ça :

    Nous allons maintenant intégrer un fichier en JSON dans notre projet afin de pouvoir l’exploiter.
    1. Sur JSON_Tuto faire ctrl+Clic > New File, pour ajouter un nouveau fichier où nous écrirons le JSON.
    2. Choisir ensuite other dans la colonne de gauche puis Empty File.
    3. Vous pouvez l’appeler JSON.txt par exemple et ensuite y intégrer les informations que nous avons utilisées dans notre comparaison entre le JSON et le XML.
    Vous devriez obtenir quelque chose de comparable à ceci :

     

    Nous allons maintenant afficher ces données dans une UITableView. Pour ce faire : ctrl+Clic > > New File. Choisir ensuite Cocoa Touch Class > UIViewController et choisir UITableViewController subclass.
    Nommez ce fichier TableViewController.
    Il reste à ajouter cette vue aux deux Appdelegate (un pour l’iPad et l’autre pour l’iPhone). Mais vous remarquerez que chaque app delegate iPhone et iPad hérite du même. Modifions donc JSON_tutoAppDelegate.h

    #import <UIKit/UIKit.h>
     
    @class TableViewController;
     
    @interface JSON_TutoAppDelegate : NSObject <UIApplicationDelegate> {
    TableViewController *tableViewController;
    }
     
    @property (nonatomic, retain) IBOutlet UIWindow *window;
    @property (nonatomic, retain) TableViewController *tableViewController;
     
    @end

    #import "JSON_TutoAppDelegate.h"
    #import "TableViewController.h"
     
    @implementation JSON_TutoAppDelegate
     
    @synthesize window=_window;
    @synthesize tableViewController;
    - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary*)launchOptions
    {
    // Override point for customization after application launch.
    self.tableViewController = [[[TableViewController alloc] initWithStyle:UITableViewStylePlain] autorelease];
    [self.window addSubview:tableViewController.view];
    [self.window makeKeyAndVisible];
    return YES;
    }
     
    - (void)dealloc
    {
    [tableViewController release];
    [_window release];
    [super dealloc];
    }
     
    @end

    Avant d’aller plus loin, vous allez compiler votre projet pour le tester.
    C’est bon ?

    Ok, changez ça dans TableViewController.m :

    - (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation
    {
    // Return YES for supported orientations
    return YES;
    }

    Cette dernière méthode permet à votre application de fonctionner dans toutes les orientations. Il est vivement recommandé de l’implémenter dans les applications iPad, d’après le guide d’Apple concernant l’Interface Homme Machine.

    Vous devriez maintenant obtenir une liste de données vide et qui fonctionne dans toutes les orientations.

    Remplir la liste depuis le fichier JSON

    Afin de faciliter le travail et de bénéficier de toute la puissance d’un langage objet, nous allons créer un objet Joueur. Pour cela, faites ctrl+Clic > New File. Créez un fichier Objective-C class héritant de NSObject.
    Appelez cette classe Joueur tout simplement !
    Passons maintenant à l’implémentation. Notre classe Joueur va respecter la structure du JSON et va ainsi posséder un nom et un score :
    Dans Joueur.h

    #import <Foundation/Foundation.h>
     
     
    @interface Joueur : NSObject {
     
    NSString *nom;
    NSInteger score;
     
    }
     
    @property (nonatomic, retain) NSString *nom;
    @property NSInteger score;
     
    @end

    Joueur.m

    #import "Joueur.h"
     
    @implementation Joueur
     
    @synthesize nom;
    @synthesize score;
     
    -(void)dealloc {
    [nom release];
    [super dealloc];
    }
     
    @end

    Le traitement du JSON

    C’est la section la plus intéressante. Un UITableViewController nécessite un tableau contenant les données à afficher.
    Dans notre cas, nous travaillerons avec une liste d’objets Joueur.
    Dans TableViewController.h

    #import <UIKit/UIKit.h>
     
     
    @interface TableViewController : UITableViewController {
     
    NSMutableArray *dataToDisplay;
     
    }
     
    @end

    Dans le format JSON, la chose importante est de distinguer les éléments des tableaux. Voici deux règles qui permettent de s’en sortir facilement : Règle 1 : le format normal est :

    "clé1" "élément1""clé2" "élément2" }

    Règle 2 : un élément peut être un tableau d’éléments et un tableau s’écrit :

    [{ "sous_clé1" "sous_élément1"} , {"sous_clé2" "sous_élément2"}]

    Le réflexe est donc [ ] donne un tableau.

    Dans notre cas, dans la clé resultat, on a un élément avec la clé Joueur qui contient un tableau de joueurs !
    Passons au code :
    Dans le .m, ajouter

    #import "JSON.h"
    #import "Joueur.h"

    Ensuite,

    - (void)viewDidLoad
    {
    [super viewDidLoad];
     
    dataToDisplay = [[NSMutableArray alloc] init];
     
    //récupération du chemin vers le fichier contenant le JSON
    NSString *filePath = [[NSBundle mainBundle] pathForResource:@"JSON" ofType:@"txt"];
     
    //création d'un string avec le contenu du JSON
    NSString *myJSON = [[NSString alloc] initWithContentsOfFile:filePath encoding:NSUTF8StringEncodingerror:NULL];   
     
    //Parsage du JSON à l'aide du framework importé
    NSDictionary *json    = [myJSON JSONValue];
     
    //récupération  des résultats
    NSDictionary *resultats    = [json objectForKey:@"resultats"];
     
    //récupération du tableau de Jouers
    NSArray *listeJoueur    =  [resultats objectForKey:@"joueurs"];
     
    //On parcourt la liste de joueurs
    for (NSDictionary *dic in listeJoueur) {
     
    //création d'un objet Joueur
    Joueur *joueur = [[Joueur alloc] init];
     
    //renseignement du nom
    joueur.nom = [dic objectForKey:@"nom"];
     
    //renseingement du score
    joueur.score = [[dic objectForKey:@"score"] intValue];
     
    //ajout à la liste
    [dataToDisplay addObject:joueur];
     
    //libération de la mémoire
    [joueur release];
    }
     
    //à ne pas oublier après l'allocation effectuée au début
    [myJSON release];
    }

    Puis

    - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
    {
    // Return the number of sections.
    return 1;
    }
     
    - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
    {
    // Return the number of rows in the section.
    return [dataToDisplay count];
    }
     
    - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
    {
    static NSString *CellIdentifier = @"Cell";
     
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
    if (cell == nil) {
    cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease];
    }
     
    Joueur *joueur = [dataToDisplay objectAtIndex:indexPath.row];
     
    //on affiche le nom et le score
    cell.textLabel.text =  [NSString stringWithFormat:@"%@ - %d",joueur.nom, joueur.score];
     
    return cell;
    }

    Et ne pas oublier le dealloc !!

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

    Voilà, vous savez maintenant utiliser le JSON et le XML !