Parser du JSON

Bonjour,


J'ai un problème avec mon application en cours développement, je vous explique:


Je développe une application pour un site web Wordpress. J'ai donc cherché une manière d'accéder aux articles depuis mon appli et je me suis tourné vers la methode JSON. J'ai donc téléchargé le plugin Feed JSON qui génère un fichier JSON avec les articles du blog. Mon problème maintenant est que je n'arrive pas à  récupérer ces données dans mon code... J'ai testé des dizaines de lignes de codes sans succès et j'ai passé des heures sur Google, c'est pourquoi je demande votre aide ! :'(


Merci d'avance !


 


PS: Et est-il possible de stocker les données récupérer dans l'iPhone pour un accès hors-connexion ?


«1

Réponses

  • Bonjour et bienvenu sur CocoaCafe.

    Pourrais-tu te présenter dans le forum adéquat afin qu'on en sache plus sur toi, notamment sur tes connaissances en dév' Cocoa, voire en POO, voire en dév' tout court afin de mieux cibler le " niveau " de nos réponses.


    Quel est ton problème exact ?

    Comment récupères-tu ton JSON ? Quelles sont les erreurs ?


    Un JSON, c'est juste une combinaison de NSArray et de NSDictionary (qui peut être plus ou moins complexe) si on veut mettre ça au niveau Objective-C. Donc quel est ton problème ?


  • Bonjour Larme,


    Je me présenterais sous peu mais sachez que je suis débutant en Objective-C :)


    En fait, mon problème est que je ne trouve pas comment récupérer proprement les données JSON et les rendre utilisables par le code. Je sais qu'il faut un NSURL qui est passé à  un NSData, qui est converti avec NSSerialization (merci yoann ;)), ... mais je suis un peu perdu dans tout ça: je débute.


    Pourriez-vous m'éclaircir s'il vous plait ?


  • AliGatorAliGator Membre, Modérateur
    Sans connaà®tre ni vraiment ton niveau (depuis combien de temps fais tu de la programmation en général, connais-tu les concepts de la POO, as-tu déjà  programmé dans d'autres langages, etc), ni les tutos que tu as suivi, les bouquins que tu as lu, la formation que tu as suivi, ni encore ce que tu as essayé comme code jusque là  avant de venir nous voir, on va avoir beaucoup de mal à  te répondre correctement ;)

    D'où l'intérêt de passer d'abord par la case "Présentation" avant de pouvoir aller plus loin.
  • Voilà  le lien de ma présentation ;)


    (Mais je ne pense pas que répondre à  ma question nécessite toutes ces infos)


  • AliGatorAliGator Membre, Modérateur
    Heu si clairement ça nous aide à  réondre à  ta question, on y apprend que tu as déjà  fait de la POO, un peu de Java, et donc on sait mieux de quelle façon on va t'expliquer les choses, que tu as déjà  lu qques tutos et as des bases.

    Et donc sinon, les lignes de code que tu as testé jusqu'ici c'est quoi, qu'on t'aide sur ce problème spécifique ?

    Comment as-tu décomposé les opérations ? Il faut bien évidemment décomposer ton problème étape par étape si tu veux comprendre comment ça marche et ce qui te coince. Par exemple commence par essayer de télécharger tes données JSON à  partir de l'URL du flux que tu veux récupérer. Puis dans un 2ème temps tu essayeras de parser ces données récupérées. Qu'as-tu essayé jusqu'à  présent ?
  • J'ai essayé pas mal de codes qui avaient tous la même forme et le dernier était celui-ci:



    NSData *data = [[NSData alloc] initWithContentsOfURL:[NSURL URLWithString:@http://frenchmac.com/feed/json]];
    NSError *error;
    NSMutableDictionary *articles = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableContainers error:&error];
    if (error) {
    NSLog(@%@", [error localizedDescription]);
    }
    else {
    for (NSDictionary *article in articles) {
    NSLog(@----);
    NSLog(@Title: %@", article[@title] );
    NSLog(@Content: %@", article[@content] );
    NSLog(@Date: %@", article[@date] );
    NSLog(@Author: %@", article[@author] );
    NSLog(@Thumbnail: %@", article[@thumbnail] );
    NSLog(@----);
    }
    }

    Et voilà  le résultat que j'ai eu dans la console:



    2014-04-23 01:48:54.352 frenchmac[21823:60b] The operation couldn't be completed. (Cocoa error 3840.)

    Donc je vois qu'il n'arrive pas à  récupérer le JSON (et c'était pareil à  chaque) mais je ne comprends pas pourquoi...


  • AliGatorAliGator Membre, Modérateur
    avril 2014 modifié #9
    Ah ben voilà , on y voit déjà  bcp plus clair une fois que tu nous dit ce que tu as essayé !
     

    Donc je vois qu'il n'arrive pas à  récupérer le JSON (et c'était pareil à  chaque) mais je ne comprends pas pourquoi...

    Heu non pas tout à  fait, il arrive très bien à  récupérer le contenu de ton URL (data). C'est juste que ce contenu en question (ces NSData que tu as récupérées), il n'arrive pas ensuite à  l'interpréter/le parser comme étant du JSON.

    Et en effet c'est normal que ça ne marche pas : on peut très facilement voir que ton URL ne renvoies même pas du JSON valide.
    • Le Content-Type retourné lorsque l'on requête http://frenchmac.com/feed/json c'est "text/html" au lieu d'être "text/json", déjà , ça commence mal. Du coup ton site déclare non pas te retourner du JSON, mais déclare te retourner du HTML, moi je serais sceptique à  partir de là  quant au fait que c'est vraiment du JSON officiel qui est retourné du coup... (on peut voir les headers par exemple avec Safari ou Google Chrome, ou en utilisant "curl -I" ou en regarde ça par code dans l'appli en inspectant les headers de la NSURLResponse retournée par NSURLConnection, etc... bref tous les moyens sont bons)
    • Mais surtout, même si on récupère quand même le contenu de la page, ce contenu n'est pas du JSON valide (On peut le voir en utilisant des sites comme http://jsonlint.com/ ou http://jsonformatter.curiousconcept.com/ par exemple). En effet il y a des sauts à  la ligne qui font foirer le bon formattage, entre autres choses, ce qui fait que le JSON généré par ton site est au final corrompu / invalide. Donc effectivement, ça peut peut-être ressembler à  première vue à  du JSON, mais cela n'en respecte pas strictement le format
    Bref, ton problème n'est pas vraiment côté Objective-C/Cocoa, mais au niveau de ton site web et de ta page derrière http://frenchmac.com/feed/json qui ne génère pas un JSON valide. Tu pourras essayer de le parser en Cocoa avec NSJSONSerialization, en Javascript, en Java, en Ruby, via un site web dédié à  ça, ou avec ce que tu veux d'autre, ça ne passera donc jamais puisque l'entrée est invalide (c'est d'ailleurs ce à  quoi correspond l'erreur 3840 si on cherche un peu sur Google ce code)

    (En résumé, le plug-in Feed JSON que tu utilise ça a l'air un peu beaucoup foireux et développé avec les pieds, ne générant ni un bon Content-Type ni un contenu valide!)




    Bon sinon, pour le reste dans ton code tu utilises dataWithContentsOfURL: ce qui est fortement déconseillé car c'est une méthode synchrone, autrement dit elle va bloquer ton main thread pendant tout le temps où elle va télécharger le contenu de l'URL pour te retourner un NSData. Donc si tu as un réseau pas très rapide et que ça met 2mn à  télécharger le contenu de ton URL, ton appli va geler (avec roue multicolore et tout) pendant tout ce temps car le main thread sera bloqué et donc l'UI aussi.

    Utilise plutôt NSURLConnection ou bien NSURLSession pour télécharger le contenu d'une URL. Tout est décrit en détail dans le URL Loading System Programming Guide de la doc Apple, avec explication des concepts en détail, code d'exemple à  la clé... bref parmi les documents incontournables (comme la plupart des Programming Guides de la doc) qu'il faut lire (entre autres).
  • TheFlow_TheFlow_ Membre
    avril 2014 modifié #10

    Ok, donc si j'ai bien compris, je dois changer de plugin car il ne renvoie pas du JSON valide et je dois utiliser NSURLSession.


    Pour ce dernier, mon code devrait ressembler à  ça ?



    NSURL *url = [NSURL URLWithString: @http://www.example.com/];
    NSURLSessionDataTask *dataTask = [self.defaultSession dataTaskWithURL: url];
    [dataTask resume];

    Mais ensuite, comme je passe les données à  NSSerialization ?


  • AliGatorAliGator Membre, Modérateur
    avril 2014 modifié #11
    Alors,

    NSURLSession
    NSURLSession est bien, par contre fais attention il n'existe que depuis iOS7. C'est la méthode préconisée maintenant, mais si ton appli doit encore pouvoir fonctionner sur iOS6 ou inférieur, tu ne pourras pas utiliser cette classe.

    Si tu choisis d'utiliser NSURLSession, et que tu veux récupérer les données retournées par la requête (ce qui est quand même un peu le but d'envoyer une requête, pour récupérer les données ^^) tu peux :

    1) Soit affecter un delegate à  la NSURLSession (pour gérer toutes les réponses de toutes les requêtes de cette session) ou bien juste à  la NSURLSessionDataTask (pour ne gérer que les réponses de cette requête/dataTask).
    Mais bon les delegate c'est pas super pratique, ça t'oblige à  récupérer la réponse dans une autre méthode plus bas, c'est pas top et un peu old-school donc c'est pas la méthode que je te conseillerai

    2) Soit passer un completionHandler à  ta NSURLSessionDataTask quand tu la crées, qui est juste un block donnant le code à  exécuter de manière asynchrone quand la tâche sera terminée (quand la réponse aura été reçue)

    Du coup utilise un truc comme ça :
    [self.defaultSession dataTaskWithURL:url
    completionHandler:^(NSData *data,
    NSURLResponse *response,
    NSError *error) {
    // Code à  exécuter quand la réponse sera arrivée
    // C'est par exemple ici que tu vas récupérer les NSData
    // et la passer à  NSJSONSerialization pour la parser
    }];
    Note : plutôt que de créer une NSURLSession et la mettre dans une @property defaultSession, tu peux aussi utiliser la [NSURLSession sharedSession] qui est pratique pour quand tu ne veux pas créer des sessions avec des configurations particulières ni n'a besoin de cloisonner les environnement d'exécution de tes requêtes.


    ---


    NSURLConnection

    Si ton application doit encore être compatible iOS6, où NSURLSession n'existait pas encore, il te faut passer par NSURLConnection (que tu peux aussi utiliser pour iOS7 d'ailleurs, ça continue de marcher et continuera encore un bout de temps je pense, c'est juste que NSURLSession est plus récent et la nouvelle préconisation mais l'ancienne méthode avec NSURLConnection va encore exister un bon moment)

    Pour ça tu as des méthodes similaires avec des completionHandler pour passer le block à  exécuter à  la fin de la requête, donc c'est pas plus compliqué non plus :
    NSURLRequest* request = [NSURLRequest requestWithURL:...];
    // Exécuter la requête de façon asynchrone, laisser iOS gérer la récupération de la réponse
    // et appeler le block de code passé au paramètre "completionHandler:" quand il aura fini.
    [NSURLConnection sendAsynchronousRequest:request
    queue:[NSOperationQueue mainQueue]
    completionHandler:^(NSURLResponse* response, NSData* data, NSError* error)
    {
    // Code à  exécuter quand la réponse de la requête est arrivée
    // Ici tu vas parser tes NSData avec NSJSONSerializer
    }];
  • Ok donc si je veux que mon appli soit compatible iOS 6<, il vaudrait mieux utiliser NSURLConnection par exemple ? Et il s'utilise comme ça ?



    // Create the request.
    NSURLRequest *theRequest=[NSURLRequest requestWithURL:[NSURL URLWithString:@http://www.apple.com/] cachePolicy:NSURLRequestUseProtocolCachePolicy timeoutInterval:60.0];

    // Create the NSMutableData to hold the received data.
    // receivedData is an instance variable declared elsewhere.
    receivedData = [NSMutableData dataWithCapacity: 0];

    // create the connection with the request
    // and start loading the data
    NSURLConnection *theConnection = [[NSURLConnection alloc] initWithRequest:theRequest delegate:self];
    if (!theConnection) {
    // Release the receivedData object.
    receivedData = nil;
    // Inform the user that the connection failed.
    }

    Et ensuite je dois utiliser la methode



    - (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data;

    pour récupérer un NSData et mettre mon NSSerialization dedans ?


     


    PS: je crois qu'il manque un bout de ta réponse ;)


  • Ton code est différent de celui que j'ai trouvé sur la doc d'Apple pour NSURLConnection. Lequel est le mieux ?


    En tout cas merci beaucoup !

    Dernière chose, comment sauvegarder ces données dans l'iPhone (avec Core Data, je crois) ?
  • AliGatorAliGator Membre, Modérateur
    avril 2014 modifié #14
    Alors la solution que tu donnes avec NSURLConnection et l'utilisation d'un delegate est valable (tu as un sample code complet dans la doc Apple normalement pour cette solution NSURLConnection + delegate), mais pas la plus pratique.

    En effet, utiliser un delegate nécessite que tu lances la requête à  un endroit de ton code, et que tu récupères les données à  un autre endroit dans une autre méthode, tu as plusieurs méthodes de delegate à  implémenter pour que ça marche correctement... si tu as plusieurs requêtes à  envoyer en parallèle et que tu utilises le même delegate pour chaque ça peut vite aussi devenir compliqué si tout le monde partage les mêmes implémentations des méthodes de delegate... Bref ça éparpille un peu les choses.

    Cette solution utilisant un delegate date du temps où les "blocks" n'existaient pas en Objective-C, mais maintenant que les blocks existent, permettant de carrément passer des "blocks de code" en paramètre (un peu comme le concept des "lambdas" ou "fonctions anonymes" dans d'autres langages, si ça te dis qqch), c'est plus simple de les utiliser que d'utiliser un delegate.


    Du coup c'est pour cela que je te conseille plutôt l'approche utilisant le paramètre completionHandler, méthode plus moderne que la solution avec les delegate, permettant de laisser NSURLConnection faire tout le boulot et directement fournir un block de code à  exécuter quand tout est fini et qu'il a récupéré toutes les données.

    C'est l'exemple que j'ai donné dans ma réponse plus haut (que j'ai édité depuis, suite à  une fausse manip' je l'avais postée trop tôt mais j'ai fini de la rédiger depuis)
  • D'accord, merci beaucoup je vais utiliser cette méthode !


    Et est-ce qu'il est possible de sauvegarder les données récupérées dans l'iPhone afin d'y avoir accès hors-connexion ?
  • AliGatorAliGator Membre, Modérateur
    Oui, mais c'est un autre sujet, donc je te conseille d'ouvrir un autre POST car là  on dérive du sujet originel.
  • D'accord ! Merci ! :D
  • TheFlow_TheFlow_ Membre
    avril 2014 modifié #18

    Après avoir testé ce code, j'ai l'impression que je ne sort pas les accolades du NSURLConnexion... Que faire ?



    NSURLRequest* request = [NSURLRequest requestWithURL:[NSURL URLWithString:@http://www.monsite.com/json]];
    // Exécuter la requête de façon asynchrone, laisser iOS gérer la récupération de la réponse
    // et appeler le block de code passé au paramètre "completionHandler:" quand il aura fini.
    [NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse* response, NSData* data, NSError* error) {
    id jsonObjects = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableContainers error:&error];
    if (error) {
    NSLog(@%@", [error localizedDescription]);
    }
    else {
    for (NSDictionary *dataDict in jsonObjects) {
    // Récupère mes données et les stocke dans un Array
    }
    }
    }];

    J'ai testé avec des NSLog, et c'est effectivement le cas...


  • AliGatorAliGator Membre, Modérateur
    J'ia pas compris la question.
  • TheFlow_TheFlow_ Membre
    avril 2014 modifié #20

    Pourquoi le code dans NSURLConnection s'éxécute-t-il en dernier ?


  • AliGatorAliGator Membre, Modérateur
    Bah c'est expliqué dans mon commentaire.

    C'est le principe des blocks qu'on utilise comme completionHandler.


    La méthode sendAsynchronous... s'exécute de façon asynchrone, non bloquante, laissant le code qu'il y a après tout ça s'exécuter normalement à  la suite. Le block que tu passes en paramètre du completionHandler, lui, c'est le code à  exécuter plus tard, seulement quand la requête aura fini.


    Je t'invite à  lire le Blocks Programming Guide pour + de détails. Ou de rechercher "blocks" sur le forum car on en a déjà  parlé dans plein de sujets et expliqué tout ça.
  • Parce-que en fait moi je veux afficher ce que je récupère dans une TableView...


  • Je ne vois pas le soucis. Tant que tu n'as pas récupérer les données, bah, tu ne fais rien.

    Quand tu les récupères, tu fais un reloadData de ta TableView.

    Si tu ne faisais pas d'async, ton UI serait bloquée. L'utilisateur penserait que ton application a planté, et du coup, c'est mauvais.


  • Cela ne semble pas fonctionner quand je fais



    [self.tableView reloadData];
  • Oui, mais là , ça va être plus complexe si tu ne fais pas un minimum d'effort de débuggage, ou nous dit ce que tu as vérifié/donnes infos.

    Premièrement, où as-tu mis cette ligne de code ?

    Est-ce que tu as parsé correctement les données JSON ?


    Est-ce que cela rentre en accord avec la manière dont le dataSource gère ça ?


    Etc.


  • TheFlow_TheFlow_ Membre
    avril 2014 modifié #26

    Voilà  mon code:



    NSURLRequest* request = [NSURLRequest requestWithURL:[NSURL URLWithString:@http://www.stationaute.com/api/highways]];
    // Exécuter la requête de façon asynchrone, laisser iOS gérer la récupération de la réponse
    // et appeler le block de code passé au paramètre "completionHandler:" quand il aura fini.
    [NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse* response, NSData* data, NSError* error) {
    id jsonObjects = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableContainers error:&error];
    if (error) {
    NSLog(@%@", [error localizedDescription]);
    }
    else {
    for (NSDictionary *dataDict in jsonObjects) {
    NSLog(@----);
    NSLog(@Id: %@", [dataDict objectForKey:@Id]);
    NSLog(@Name: %@", [dataDict objectForKey:@Name]);
    NSLog(@Direction: %@", [dataDict objectForKey:@Direction]);
    NSLog(@Label: %@", [dataDict objectForKey:@Label]);

    [autoroutes setObject:[dataDict objectForKey:@Id] forKey:Id];
    [autoroutes setObject:[dataDict objectForKey:@Name] forKey:Name];
    [autoroutes setObject:[dataDict objectForKey:@Direction] forKey:Direction];
    [autoroutes setObject:[dataDict objectForKey:@Label] forKey:Label];
    [myObject addObject:autoroutes];
    }
    }
    NSLog(@Ok);
    [self.tableView reloadData];
    }];

    Quand je l'exécute, je peux voir que mes données sont bien récupérées grâce à  la console et j'ai rajouté le NSLog de la ligne 24 pour vérifier que je sors des accolades. Par contre, ma Table View reste vide malgré la ligne 25 qui est sensée la recharger.


  • Et comment tu remplis ta TableView ?


  • Comme ça:



    - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
    {
    // Return the number of rows in the section.
    return myObject.count;
    }

    - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
    {
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@AutorouteCell forIndexPath:indexPath];

    NSDictionary *tmpDict = [myObject objectAtIndex:indexPath.row];

    NSString *title = [NSString stringWithFormat:@%@",[tmpDict objectForKeyedSubscript:Name]];
    NSString *subtitle = [NSString stringWithFormat:@%@",[tmpDict objectForKeyedSubscript:Label]];
    NSString *right_detail = [NSString stringWithFormat:@%@",[tmpDict objectForKeyedSubscript:Direction]];
    if ([subtitle isEqual:@<null>]) {
    subtitle = @"";
    }

    UILabel *TitleLabel = (UILabel *)[cell viewWithTag:1];
    TitleLabel.text = title;

    UILabel *SubtitleLabel = (UILabel *)[cell viewWithTag:2];
    SubtitleLabel.text = subtitle;

    UILabel *RightDetailLabel = (UILabel *)[cell viewWithTag:3];
    RightDetailLabel.text = right_detail;

    return cell;
    }
  • AliGatorAliGator Membre, Modérateur
    Je parie que myObject est nil. T'as dû oublier de l'allouer.
  • TheFlow_TheFlow_ Membre
    avril 2014 modifié #30

    C'est bon, j'ai trouvé le problème ! Cela ne venait pas du reloadData mais de mon NSMutableArray myObject qui n'était pas instancié... En tout cas, merci !


    EDIT: Oui, AliGator, merci ;)


  • Et si on a pas de table view mais un label simple, comment on recharge ?


Connectez-vous ou Inscrivez-vous pour répondre.