[Résolu] Chargement d'un XML volumineux : En background - Multi view

13»

Réponses

  • AliGatorAliGator Membre, Modérateur
    Voilà .

    Bon après y'aura encore d'autres choses à  potentiellement travailler sur le sujet pour rendre ça parfait, mais je vais pas t'embrouiller d'avantage, déjà  si tu implémentes ça ça sera déjà  un grand pas en avant ;)
  • Am_MeAm_Me Membre
    septembre 2014 modifié #63

    Bah au moins j'aurai compris à  quoi servent réellement les blocs et voir comment ça fonctionne. Dans un futur proche je me lancerai dans du concret pour décortiquer les petits détails faisant des blocs un principe qui m'intéresse énormément.


     


    On appris en cours de java plusieurs modèle MVC (Push, Pull) bref là  n'est pas l'important.


     


    1) En pratique en java j'avais l'habitude de mettre dans mon Controler un pointeur vers mon modèle. Et si je faisait la même chose ici : Aurai-je raison ?


     


    2) Sachant que c'est mon singleton qui va exécuter la méthode du modèle (-executeRequete) et non pas mon VC (lui il exécutera traiteXML du singleton) alors je me disais que j'avais plusieurs solutions qui s'offre à  moi : 


    • a) Passer le modèle en paramètre de la méthode -traiteXML pour qu'il puisse faire -executeRequete
    • b )Faire de la méthode executeRequete (du modèle) une méthode +
    • c) Avoir un pointeur vers mon modèle dans mon singleton
    • d) .... ?

    D'ailleurs ma méthode traiteXML va devoir contenir un block de même :)


  • AliGatorAliGator Membre, Modérateur
    Alors c'est là  que les avis divergent, et qu'éventuellement je te parlais de potentiellement de nouvelles choses à  travailler sur le sujet.

    Personnellement, pour toutes les méthodes transverses offrant ce que j'appelle des "Services" " je met dans cette catégorie par exemple tout ce qui concerne l'interaction avec les WebServices, le parsing du résultat, le remplissage du modèle avec ce résultat parsé, ... mais aussi par exemple un "Service" qui va me dire si j'ai du réseau ou pas, un "Service" qui va me gérer les préférences de l'utilisateur pour savoir de n'importe où s'il a activé telle ou telle pref, etc " je fais des sharedInstance (Singleton, si tu préfères, bien que dans le cas d'ObjC ça soit un abus de langage mais qui reste assez usité).

    J'évite les SharedInstances quand elles ne sont pas justifiées, c'est le genre de pattern qu'il faut éviter d'utiliser à  toutes les sauces à  part si c'est vraiment utile. Mais pour le cas des "Services" c'est le rare cas où je me tolère cet usage.
    Ainsi, je vais plutôt avoir une SharedInstance "MyWebService" qui va contenir que des méthodes de classe du style "fetchContactsWithCompletion:..." qui va se charger d'effectuer la requête en arrière plan (avec NSURLSession par exemple) au WebServices pour récupérer les contacts (si par exemple je fais une application de Carnet d'addresses), puis parser le résultat, remplir le modèle (CoreData en général de mon côté) et m'appeler le CompletionBlock pour me dire que c'est fini.

    D'autres personnes sont moins adeptes de cette vision d'utiliser des SharedInstance pour les "Services", et préfèrent l'injection de dépendance, c'est à  dire passer leur modèle ainsi que leur instance qui gère les requêtes de proche en proche à  chaque ViewController qui en a besoin. Ca veut dire que chaque ViewController va devoir avoir une @property pour qu'on puisse lui passer le modèle en paramètre avant de le présenter, pour qu'il puisse l'utiliser quand il en a besoin. Ca se défend aussi, ça permet d'éviter le pattern Singleton (SharedInstance) et de décorreler mieux les responsabilités, mais du coup il faut passer ton modèle à  chaque VC à  chaque fois.

    D'autres vont mettre les méthodes qui exécutent les requêtes dans la classe du modèle (ne plus avoir un singleton pour les objets et le modèle, mais mettre tes méthodes dans le modèle et ne plus avoir de singleton)...

    En terme d'architecture logicielle, il n'y a pas de bonne réponse absolue. Il n'y a que des bonnes pratiques et des mauvaises pratiques, des patterns et des concepts déjà  éprouvés qui ont montré leurs avantages, mais après un architecte aura une vision souvent un peu différente d'un autre architecte. Personnellement je préfère mettre mes Services sous forme de SharedInstance/Singleton, d'autres aiment moi, chacun sa vision et ses préférences.
    L'important c'est de se poser, réfléchir, ne pas hésiter à  prendre papier et crayon, et faire en sorte que les responsabilités de chaque classe soient bien décorrelées (en particulier ne pas mélanger le modèle (M), sa représentation visuelle (V) et l'intelligence de ton application (C))



    En l'occurrence pour ton cas je n'ai pas bien compris quelle était la responsabilité de ta méthode traiteXML. Je suppose que cette méthode a pour but d'exploiter les données " déjà  récupérées et parsées et stockées dans le modèle par effectueRequete " pour mettre à  jour l'affichage en conséquence ?
    Si oui son nom n'est pas forcément bien choisi, car du point de vue du VC tu ne vas manipuler que des objets du modèle. Que ces objets viennent d'un XML ou pas à  l'origine ne change rien.
    Si demain ton WebService te retourne du JSON et non plus du XML, tu vas alors modifier ta méthode executeRequete en conséquence pour qu'elle n'utilise plus un NSXMLParser et [parser parse] et tout, mais pour qu'elle lise du JSON à  la place, mais dans tous les cas à  la fin tu mettras les données parsées dans ton modèle, représenté sous forme d'objets comme typiquement des sous-classes dédiées de NSObject. Et bah tout ça ne changera rien du point de vue de ton ViewController, qui continuera d'exploiter les mêmes objets du modèle pour mettre à  jour son interface. La méthode devrait plutôt s'appeler qqch comme "updateViewFromModel" par exemple.

    Et du coup, pour moi la logique serait plutôt que ton SearchViewController appelle lui-même la méthode "executeRequest:completion:", et dans le completionBlock, qui va lui signaler que la requête a bien été faite, la réponse parsée et le modèle mis à  jour, tu appellerais alors la méthode "updateViewFromModel" pour mettre à  jour l'écran avec les nouvelles données.
    Pour aller plus loin dans le nommage, tu pourrais imaginer renommer ta méthode "executeRequete:completion:" en "updateModelWithRequest:completion:" qui serait peut-être plus juste, puisque c'est une méthode qui envoie une requête pour permettre de mettre à  jour le modèle avec la réponse.

    La logique serait donc que le SearchViewController demande à  mettre à  jour le modèle avec le WebService, et une fois que le modèle est mis à  jour (completion), demande à  mettre à  jour la vue avec le nouveau modèle. Donc une logique du genre : [model updateModelWithRequest:... completion:^{ [self updateViewWithModel] }]. Personnellement je trouve que ça clarifie le cheminement.

    Reste à  toi de décider si cette variable "model" que j'ai utilisé ici tu préfères que ce soit le modèle que tu passes de proche en proche de ViewController à  ViewController (et ne plus avoir de SharedInstance) comme font certains, ou si tu préfères éviter d'avoir à  le passer systématiquement et que tu préfères en faire un Singleton (SharedInstance) pour qu'il soit accessible de n'importe où (ce que personnellement j'ai l'habitude de préférer).

    Ou encore de faire ce que tu as proposé aussi, à  savoir séparer la partie "Envoi de requête et parsing de la réponse" de la partie "modèle". C'est pas plus mal non plus ça aussi. Et du coup mettre la méthode "updateModel:withRequest:completion:" dans ton singleton NetworkService (ou tu l'appelles comme tu veux) dédié à  la partie envoie de requête + parsing de la réponse. Et passer le modèle en paramètre à  cette méthode à  chaque fois que tu l'appelles. Quitte à  devoir passer le modèle, lui, de proche en proche à  chaque ViewController. Comme ça la partie Réseau/Interaction avec le WebService est séparé dans un "Service" à  part dans un Singleton, et la partie Modèle est d'un autre côté, au final c'est aussi assez propre. Ou alors, cette même solution de séparer Modèle et Service, mais faire de modèle *aussi* un singleton pour t'éviter d'avoir à  le passer de proche en proche de VC en VC.

    Bref, comme tu le vois, il n'y a pas forcément une solution meilleure qu'une autre dans l'absolu, chaque architecte a sa vision des choses je t'ai exposé je pense à  peu près toutes les solutions qui me semblent viables et correctes au niveau structuration. Personnellement en général je pars sur une structure qui colle plutôt avec ma dernière proposition, mais à  toi de voir si tu préfères les SharedInstance/Singleton ou le passage de proche en proche (injection de dépendances), tant pour le Service que pour le Modèle.
  • Am_MeAm_Me Membre
    septembre 2014 modifié #65

    L'idée de faire le modèle en tant que singleton me plait énormément ! vu que ça m'évitera de me le trimbaler un peu partout.


     


    De plus en fait c'est de ma faute je vous ai emmené dans un cas qui ne correspond pas trop à  la logique de mon application : en gros ce que je vous ai dit c'est que je voulais que la requête survivent entre le passage du SearchVC et HomeVC, ça c'est vrai.


     


    Mais entre les 2 je rafraichi la vue certes. Mais pas par l'intermédiaire de mon traiteXML.


    En fait dans mon SearchVC je charge un autre xml qui lui rempli une tableView. Je rempli un tableau d'objet et chaque ligne de ce tableau correspond à  une cellule du tableau. 


     


    ________


     


    Bon c'est du charabia mais je vais te donner un vrai exemple concret.


    Un exemple c'es toujours mieux pour comprendre le tout


    Supposons un objet : 



    @class Article:NSObject
    {
    NSString *nom,*url,texte;
    }

    Imagine que dans mon SearchVC l'utilisateur peut ajouter des liens RSS. Il cherche par exemple "Coupe du monde 2014" et ça va lui retourner une liste d'articles.


    Donc ma table va contenir des cellules et chaque cellules correspond à  un lien vers un article ("donc ma cellule contient juste le nom de l'article et url vers le site en question rien de plus donc en gros j'aurai initialiser Article.nom et Article.url". Ensuite il devra cliquer sur un bouton par exemple pour ajouter l'article dans une rubrique "A lire" qui sera affiché dans mon HomeVC. J'espère que tu me suis jusque là  il m'arrive de ne pas être très clair :/ . Donc quand l'utilisateur choisi de rajouter l'article dans ses articles "A lire" cela  je charge d'autres information sur mon objet Article supposons je charge le texte donc Article.text et que son XML est énorme aussi.


     


    Et justement ce texte sera affiché dans la DetailVC. Bah en fait par logique si je donne le pointeur de mon objet à  la méthode 



    traiteXMLForArticle:(Article)*article 

     (de mon singleton) bah là  ça me va comme ça. Car même au passage vers HomeVC l'article (correspondant à  l'article passé en paramètre dans traiteXML) inséré dans la cellule du tableau "A lire"  aura le même pointeur. Du coup mon XML aura déjà  été chargé quand je serai sur la HomeVC vu que le parse aura survécu (et le parse va initialiser mon Article.text) . Du coup quand l'utilisateur clique sur la HomeVC sur l'article en question pour le lire, il sera déjà  chargé.


     


    C'est vrai qu'au départ je vous avais un peu embrouillé. Mais du coup oui maintenant que je vais passer au MVC forcément je vais passer par le refresh de la view : je pense notamment à  la partie recherche "Coupe du monde 2014" je devrai demander au modele de traiter la recherche et de mettre ma vue à  jour quand les articles seront chargés.


     


     


    Si je n'ai pas été clair dans mon exemple n'hésite pas à  me reprendre.


  • denis_13denis_13 Membre
    septembre 2014 modifié #66

    au départ j'ai trouvé le schéma pas très explicite, mais je dois manquer de culture en schématisation... Bref ton projet a bien pu évoluer et c'est normal, c'est d'ailleurs le support des évolutions qui nous guide l'architecture des programmes (en principe...). La constante semble être la mise en cache, une instance partagée était donc une bonne piste et le fait de déclencher la recherche du contenu dans le dernier contrôleur a l'avantage de ne pas consommer des resources pour un contenu non désiré (je crois que l'on est tous d'accord sur le sujet). Il faut juste que tu t'arrange pour distinguer trois cas, tes données sont déjà  dans le cache ou sont en cours de chargement ou doivent être retrouvées (soit elles n'ont pas encore été cherchées soit elles ont été effacées).




  • Il faut juste que tu t'arrange pour distinguer trois cas, tes données sont déjà  dans le cache ou sont en cours de chargement ou doivent être retrouvées (soit elles n'ont pas encore été cherchées soit elles ont été effacées).




     


    Oui voilà  le plus important c'est que dans ma vue DetailVC je vérifie si les données ont déjà  été chargé. Si je reprend mon exemple il faut voir si le texte de l'article a déjà  été chargé : sinon je relance une requête pour récupérer le texte de l'article.

  • dans ton modèle tu peux utiliser un NSMutableDictionary ou un NSCache et créer des entrées en début de chargement en ajoutant un objet provisoire (qui lui est un singleton, par exemple NSNull) te permettant de tester ensuite si un chargement est en cours. Donc quand tu arrives sur ton contrôleur DetailVC, tu testes la valeur associée à  ta clef dans ton dictionnaire ou ton cache si nil, tu lances une recherche si une valeur existe tu l'utilises à  moins qu'elle ne soit égale à  [NSNull null], dans ce cas la recherche est en cours (et tu pleures de ne pas avoir implémenté d'observation...)


  • AliGatorAliGator Membre, Modérateur

    dans ton modèle tu peux utiliser un NSMutableDictionary ou un NSCache et créer des entrées en début de chargement en ajoutant un objet provisoire (qui lui est un singleton, par exemple NSNull) te permettant de tester ensuite si un chargement est en cours. Donc quand tu arrives sur ton contrôleur DetailVC, tu testes la valeur associée à  ta clef dans ton dictionnaire ou ton cache si nil, tu lances une recherche si une valeur existe tu l'utilises à  moins qu'elle ne soit égale à  [NSNull null], dans ce cas la recherche est en cours (et tu pleures de ne pas avoir implémenté d'observation...)

    non tu ne pleures certainement pas de ne pas avoir implémenté l'observation bien au contraire puisque ça aurait fait 2 chemins différents pour récupérer la valeur, une dépendance entre les 2 classes (certes faible mais tout de même), des risques d'oublier de retier l'observer quand il est détruit, etc.


    Par contre en effet il faut implémenter un mécanisme pour quand la requête est déjà  partie pour ne pas en renvoyer une autre. Moi en général je me garde un NSMutableDictionary qui va contenir un tableau de completions à  appeler pour chaque url. Ce NSMutableDictionary est une @property (privée) de ma sharedInstance, initialisée à  un dictionnaire vide dans son init.


    Puis à  chaque fois que j'appelle executeRequest:completion:, si la clé pour l'url n'existe pas dans le dictionnaire alors je la crée et lui associe un NSMutableArray contenant comme seul élément le completionBlock, et j'effectue ma requête avec NSURLSession. Si la clé avec l'url existait déjà , ça veut dire qu'une requête est en cours, dans ce cas je me content d'ajouter le completionBlock à  NSMutableArray associé à  cette URL et je ne fais pas la requête. A la fin, dans le block de NSURLSession, après avoir reçu la requête et fait le parsing, au lieu d'appeler directement colpletionBlock(), je récupère le NSMutableArray lié à  l'URL, je l'enlève de suite du NSMutableDictionary, et je boucle sur les completionblocks du NSMutableArray recupéré pour les appeler toutes les unes après les autres.



    De ce fait, je n'ai plus dans mon VC qu'à  appeler executeRequest:completion: que si mon modèle n'a pas la donnée qui m'intéresse, sans le soucier de savoir s'il y a déjà  une requête de partie (si c'est le cas avec mon système il n'enverra pas de 2ème requête). Si mon modèle à  la donnée je l'utilise sinon j'appelle executeRequest:completion: pour le remplir. Et au pire si j'appelle executeRequest:completion: plusieurs fois de suite plus vite que les requêtes ne s'exécutent et reçoivent leur réponse c'est pas grave, une seule requête sera envoyée mais tous mes completionBlock qui ont demandé cette requête / URL seront chacun appelés.


  • Par contre en effet il faut implémenter un mécanisme pour quand la requête est déjà  partie pour ne pas en renvoyer une autre. Moi en général je me garde un NSMutableDictionary qui va contenir un tableau de completions à  appeler pour chaque url. Ce NSMutableDictionary est une @property (privée) de ma sharedInstance, initialisée à  un dictionnaire vide dans son init.




     


    Ouah ça m'intéresse ton truc. C'est un char d'assaut de l'optimisation (enfin c'est comme ça que je l'imagine   ). Mais je n'ai pas compris : un tableau de complétion  :/  : complétion le même truc que pour les blocs? je ne pige pas trop


     




    Puis à  chaque fois que j'appelle executeRequest:completion:, si la clé pour l'url n'existe pas dans le dictionnaire alors je la crée et lui associe un NSMutableArray contenant comme seul élément le completionBlock, et j'effectue ma requête avec NSURLSession. Si la clé avec l'url existait déjà , ça veut dire qu'une requête est en cours, dans ce cas je me content d'ajouter le completionBlock à  NSMutableArray associé à  cette URL et je ne fais pas la requête. A la fin, dans le block de NSURLSession, après avoir reçu la requête et fait le parsing, au lieu d'appeler directement colpletionBlock(), je récupère le NSMutableArray lié à  l'URL, je l'enlève de suite du NSMutableDictionary, et je boucle sur les completionblocks du NSMutableArray recupéré pour les appeler toutes les unes après les autres.




     


    Ouai la pareille je comprend moyennement la logique : le si existe et si n'existe pas. Mais dans le si exige tu dis "alors je la crée et lui associe un NSMutableArray contenant comme seul élément le completionBlock," même remarque qu'au dessus. 


     


    Et quand la clé url existe : en gros tu rajoute le bloc mais tu ne fait rien derrière à  quoi ça sert vu qu'il est déjà  en cours ?


     


    Faudrait que je comprenne surtout ce que tu entends par ajouter un completionBlock avant je pense (tu rajoute des lignes de code sous format de mutableArray ?)


     




    De ce fait, je n'ai plus dans mon VC qu'à  appeler executeRequest:completion: que si mon modèle n'a pas la donnée qui m'intéresse, sans le soucier de savoir s'il y a déjà  une requête de partie (si c'est le cas avec mon système il n'enverra pas de 2ème requête). Si mon modèle à  la donnée je l'utilise sinon j'appelle executeRequest:completion: pour le remplir. Et au pire si j'appelle executeRequest:completion: plusieurs fois de suite plus vite que les requêtes ne s'exécutent et reçoivent leur réponse c'est pas grave, une seule requête sera envoyée mais tous mes completionBlock qui ont demandé cette requête / URL seront chacun appelés.




     


    La je sèche  :o


     


    Ca m'intéresse énormément mais bon si tu n'a pas le temps pas grave je relierai 50 fois s'il le faut :D

  • c'est plutôt bien vu comme méthode, je vais expérimenté un peu, c'est vrais que les blocs sont des objets et que l'on peut les stocker, je n'ai pas suffisamment joué avec, je vais m'y mettre c'est intéressant (avant il fallait passer par les invocations, qui sont pas très évidentes... 


    En fait l'observation du cache est compliqué si le cache est libéré automatiquement (NSCache), mais ce n'est pas non plus ingérable... (et si le cache est dans un dico c'est trivial). Par contre 


  • AliGatorAliGator Membre, Modérateur
    @Am-Me oui les completionBlocks (enfin les blocks en général) c'est aussi vu comme des objets (c'est aussi ça qui est magique avec les blocks). Donc tu peux tout à  fait mettre un completionBlock dans un tableau (NSArray).

    Et du coup oui si la clé url existe (ce qui veut dire que la requête est déjà  partie mais pas encore arrivée) je me contente de rajouter l'objet completionBlock dans mon tableau mais je ne fais rien après. A quoi ça sert ? Bah je ne vais pas faire repartir une nouvelle requête, puisqu'il y en a déjà  une en cours. Et mettre le completion dans le tableau ça sert au fait que quand la requête qui est déjà  partie a enfin sa réponse qui arrive, bah elle ne va pas appeler que le premier completionBlock qui a été passé la première fois qu'on l'a appelé mais elle va boucler sur tous les completionBlocks qu'on a mis dans le tableau. Donc ça sert à  ce que même si on n'envoie pas la requête car il y en a déjà  une de partie, bah quand la réponse va arriver on va appeler le completionBlock quand même (le premier, celui qui est associé à  l'appel qui a envoyé la requête, mais aussi le 2e, celui qui a été passé quand on a appelé la méthode une 2ème fois sans que ça n'envoie une 2ème requête).
  • AliGatorAliGator Membre, Modérateur

    - (void)executeRequest:(NSURLRequest*)request completion:( void(^)(void) )completionBlock
    {
    NSMutableArray* completionsForURL = self.pendingCompletions[request.URL];
    if (completionsForURL)
    {
    // On a déjà  une requête de partie (et au moins une ou plusieurs completions en attente
    // d'avoir la réponse pour être appelées) donc on se rajoute juste à  la liste d'attente
    [completionsForURL addObject:completionBlock];
    // Et c'est tout, pendant ce temps la requête qui est déjà  partie est toujours en cours
    // Et tous les completions seront appelés quand elle sera finie, donc rien besoin de faire d'autre
    return;
    }

    // Sinon, c'est qu'on n'a pas de requête en cours.
    // Du coup, on initialise le tableau de toutes les completions à  appeler,
    // avec pour l'instant juste celle qu'on vient de passer en paramètre
    self.pendingCompletions[request.URL] = [NSMutableArray arrayWithObject:completion];

    // Puis on exécute la requête
    [[NSURLSession sharedSession] dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error)
    {
    // On a reçu la réponse
    ... (création du parseur, etc) puis on parse le XML (ce qui va remplir le modèle)
    [parser parse];

    // On a fini, on appelle non pas juste le completionBlock() passé lors de CET appel, mais tous
    // les completionBlocks qui ont pu être rajouté à  la liste d'attente éventuellement entre temps
    NSArray* allBlocks = self.pendingCompletions[request.URL];
    [self.pendingCompletions removeObjectForKey:request.URL]; // On vide la liste et supprime l'entrée (pour recommencer à  envoyer la requête la prochaine fois)
    // Et donc on boucle sur tous les completionBlocks qui ont été demandés à  être appelés depuis que la requête a été envoyée
    for ( void(^aCompletion)(void) in allBlocks)
    {
    aCompletion();
    }
    }];
    }
    J'ai tapé ça à  l'arrache dans le post donc j'ai pas vérifié mon code, mais c'est le principe. Du coup si tu appelles 2 ou 3 fois de suite executeRequest:completion: (avec la même request.URL à  chaque fois) avant que la réponse n'ait eu le temps d'arriver, bah la première fois ça va créer la liste (dans l'entrée pendingCompletions[request.URL] correspondante) avec le 1er completionBlock puis envoyer la requête, mais la 2ème et 3ème fois vu que la requête est toujours en cours, ça va se contenter d'ajouter le 2ème et le 3ème completionBlock à  la liste déjà  créée. Quand la réponse va arriver, les 3 completionBlocks vont être appelés l'un après l'autre. Comme ça tu n'envoies la requête qu'une seule fois, mais tu appelles bien tous les completionBlocks, et pas que celui qui a été passé à  l'appel qui a déclenché la requête.
  • Donc tu peux enregistrer par exemple : completion(); (=> si j'ai bien compris c'est ça l'objet en question.) dans l'arrayList


     


    Donc mon array pourrait contenir


    Array { objet:completion(); object:autrecompletion() }; 


     


    Mais quand tu dis 



     


     


    Et du coup oui si la clé url existe (ce qui veut dire que la requête est déjà  partie mais pas encore arrivée) je me contente de rajouter l'objet completionBlock dans mon tableau mais je ne fais rien après.

     


    Cela implique qu'il y a déjà  dans l'Array une autre ligne avec la même KEY_URL ?


    Si oui cela signifie qu'il y a déjà  la même requête dans l'Array donc tu va l'appeler 2 fois ?


     


    Enfin c'est cette phrase qui me trouble : 



     


     


     Donc ça sert à  ce que même si on n'envoie pas la requête car il y en a déjà  une de partie, bah quand la réponse va arriver on va appeler le completionBlock quand même (le premier, celui qui est associé à  l'appel qui a envoyé la requête, mais aussi le 2e, celui qui a été passé quand on a appelé la méthode une 2ème fois sans que ça n'envoie une 2ème requête).

     


    Est-ce que les completionBlock dans ton array ont un lien au final ou pas ?


  • Am_MeAm_Me Membre
    septembre 2014 modifié #75

    Ah je viens de voir ton message exemple je le lis ... (merci)


  • Ok donc quand on a reçu la réponse on parse pour remplir le modèle ça je t'ai suivi jusque là .


    Et après on exécute les requêtes qui ont pu être mise en attente dans d'autre fonction par exemple dans une autre méthode executeRequestImage // qui se charge de charger des images par exemple


     


    Mais je ne pige pas ça ou plutôt je suis  o:)



    // On a fini, on appelle non pas juste le completionBlock() passé lors de CET appel, mais tous
    // les completionBlocks qui ont pu être rajouté à  la liste d'attente éventuellement entre temps
    NSArray* allBlocks = self.pendingCompletions[request.URL];
    [self.pendingCompletions removeObjectForKey:request.URL]; // On vide la liste et supp

    Donc en gros ta méthode executeRequest exécute toute requête confondu ?


    Par exemple je fait une requête sur un fichier XML puis sur des images puis sur un  autre XML : ta fonction va les exécuter une par une.


     


    1) Mais qu'en est-il si je veux utiliser des parseurs différents par exemple.


    2) Dans la boucle tu appel les completion(); donc à  partir de là  tu débloque les blocs appelé dans tes VC par exemple un à  un c'est ça ? (juste pour être sur)

  • AliGatorAliGator Membre, Modérateur
    Attention je te rappelle que ce que j'utilise dans mon code c'est un dictionnaire, dont les clés sont les URLs, et les valeurs sont des NSMutableArray.

    Donc quand tu dis "il y a déjà  dans l'Array une autre ligne avec la même KEY_URL" ça n'a pas trop de sens, un Array n'a pas de clés. Par contre dans le cas où une requête avec cette URL est déjà  partie et est encore en cours, alors oui cela implique (et c'est d'ailleurs ce que je teste dans mon "if") qu'il y a déjà  dans mon dictionnaire pendingCompletions une valeur en face de la clé URL, et cette valeur associée à  la clé URL est un NSMutableArray contenant déjà  la première completion. Et donc dans ce cas on ne fait que rajoute une autre completion (celle du 2ème appel à  la méthode) à  cette liste / à  ce NSMutableArray associé à  l'url en question dans le dictionnaire.

    Le dictionnaire pendingCompletions a donc une structure du genre :
    @{
    @http://host/premiere-url/: @[; completion1, completion2, completion3 ],
    @http://host/deuxieme-url/: @[; completion4 ]
    }
    (S'il y a ça dans le dictionnaire pendingCompletions ça veut dire qu'on a demandé 3 fois l'URL "http://host/premiere-url/", avec 3 completions différentes completion1, completion2 et completion3, et qu'on n'a pas encore la réponse pour cette URL (la requête est toujours en cours), et qu'on a demandé une fois l'URL "http://host/deuxieme-url/" avec la completion completion4. Quand la réponse à  http://host/premiere-url/ va arriver, et qu'on va parser le résultat et remplir le modèle, on va ensuite appeler completion1() puis completion2() puis completion3() et supprimer l'entrée liée à  la clé "http://host/premiere-url/" du dictionnaire.
  • AliGatorAliGator Membre, Modérateur

    Donc en gros ta méthode executeRequest exécute toute requête confondu ?
    Par exemple je fait une requête sur un fichier XML puis sur des images puis sur un  autre XML : ta fonction va les exécuter une par une.

    Bah vu que mon dictionnaire pendingCompletions est indexé avec l'url comme clé, les completions à  exécutés sont rangées correctement, dans des listes différentes selon l'URL demandée. Donc si tu appelles "executeRequest:completion:" avec une URL donnée (celle d'un premier XML), puis rappelles "executeRequest:completion:" avec une autre URL (celle d'un autre XML) bah chacune va exécuter la bonne completion. Tu ne risques pas d'exécuter les completions dès que la première URL a fini de se charger, puisque chacune des 2 completions est rangée dans une liste séparée, indexée par la bonne URL dans le dictionnaire.
     

    1) Mais qu'en est-il si je veux utiliser des parseurs différents par exemple.

    Ah bah là  il faut faire une autre méthode (mais ça ça n'a pas grand chose à  voir avec le truc dont on discute qui est d'éviter d'envoyer plusieurs fois la même requête, même avant ça quand tu appelais directement completionBlock() après ton [parser parse] et ne gérait pas une liste de "completionBLocks en attente" avec mon code, si tu voulais utiliser un parseur différent il fallait bien avoir une méthode différente quelque part, ou passer ton parseur en paramètre, ou autre). C'est une question un peu différente de ce qui nous intéresse

    2) Dans la boucle tu appel les completion(); donc à  partir de là  tu débloque les blocs appelé dans tes VC par exemple un à  un c'est ça ? (juste pour être sur)

    Oui. Enfin que les comlpetionBlocks qui ont été appelés pour l'URL qui vient de finir. Pas tous ceux qui sont dans tout mon dictionnaire, juste ceux associés à  l'URL en question.
  • Am_MeAm_Me Membre
    septembre 2014 modifié #79

    Alors là  ok c'est un peu plus clair.


    Mais je ne pige pas le concept d'appeler 3 fois l'url @"http://host/premiere-url/": avec 3 blocs différents. Si tu l'appel 1 fois tu enregistre le résultat dans une variable ou un singleton les 2 autres complétion() devrait juste se renseigner auprès du singleton.


    Je dis ça mais je n'ai surement pas d'imagination mais ça doit être utile qq part


     


    PS : je ne vais pas t'embêter plus : peut-être que je comprendrai un jour l'utilité de cette machine de guerre


  • AliGatorAliGator Membre, Modérateur
    Le problème c'est qu'une requête réseau, ce n'est pas immédiat. Si tu as un mauvais réseau, que tu es sur un réseau Edge avec une seule barre, entre le moment où tu vas envoyer la requête et le moment où tu vas recevoir la réponse (et donc enregistrer le résultat dans une variable), il peut se passer 5 ou 10 secondes par exemple, si ton réseau n'est pas très bon. Et pendant ces 10 secondes, tu as largement le temps de rappeler la méthode executeRequest:completion: avec la même requête.

    Par exemple imagine que tu as un bouton "Refresh" sur ton interface, et que l'utilisateur s'amuse à  taper dessus comme un fou, 10 fois de suite pendant ces 5 secondes. Ou bien que tu exécutes ta requête pour récupérer ton XML listant tes articles dans le viewWillAppear de ton ViewController, et que l'utilisateur s'amuse à  aller sur cet écran, faire back, revenir sur l'écran, refaire back, revenir sur l'écran... le tout pendant ce laps de temps de 5 secondes où ta requête est déjà  partie mais est toujours en attente de sa réponse.

    Dans ce cas ça serait bien dommage de renvoyer 10 fois la requête juste parce que l'utilisateur a tapé 10x sur "Refresh". Et pourtant pendant ces 5 secondes, tu n'as toujours rien dans ton modèle, tu n'as pas encore reçu la réponse donc il n'y a aucun résultat enregistré dans ta variable. Donc les 9 autres completions ne peuvent pas juste se renseigner auprès du singleton pour avoir la valeur de la variable, puisqu'elle n'est pas encore arrivée.

    Donc il faut bien attendre que la réponse à  la requête soit arrivée pour appeler tous les completionBlocks. D'autant que le but d'un completionBlock c'est de prévenir quand les données, demandées de façon asynchrone, sont enfin arrivées, donc il ne faut pas les appeler avant sinon ça n'a pas de sens : dans le code de ton completionBlock tu vas sans doute faire un reloadData ou un truc comme ça, mais si le completionBlock est appelé trop tôt car il n'y a encore aucun résultat enregistré dans ton singleton, ça ne va servir à  rien.

    ---


    Alors certes, dans ton cas (du moins dans le seul cas auquel tu as pensé pour l'instant), tu vas certainement appeler systématiquement le même completion à  chaque fois que tu demandes une requête donnée. Par exemple tu fas appeler "[singleton executeRequest:requestPourTaListeDArticles completion:^{ [self reloadData]; }];" dans la méthode refresh de ListViewController et nulle part ailleurs, donc vu que c'est le même completionBlock à  chaque fois, si tu appelles ce completionBlock qu'une seule fois c'est pas tant que ça un problème, ta liste va bien se rafraà®chir quand la réponse à  la requête sera arrivée.

    Mais si demain tu as besoin d'appeler executeRequest:completion: avec la même requête, mais à  un endroit différent, et avec un completionBlock qui n'a rien à  voir (par exemple imagine demain tu veux charger la liste des articles en tâche de fond, et dans ton completionBlock envoyer une notification locale à  l'utilisateur quand la liste est disponible, etc), il faut bien que ça continue à  marcher. Donc si jamais, par le fruit du hasard, il se trouve que ta tâche de fond qui charge les articles commence à  10:00:00 et que l'utilisateur tape sur le bouton refresh à  10:00:01 alors que la réponse n'est pas encore arrivée, il faut bien que les 2 completions soient appelés, pour pas que tu n'aies que le completion de la tâche de fond qui envoie la notification quand la liste est dispo, mais que tu le completion qui fait le reloadData soit aussi appelé.


    Bref, dans le futur de ton application il se peut tout à  fait que tu appelles 2 ou 3 fois l'URL http://host/premiere-url/" mais avec des blocs différents.
    Et dans tous les cas si tu l'appelles une 2ème ou 3ème fois, il faut attendre la completion pour pouvoir se renseigner auprès du singleton pour avoir le résultat enregistré, car si on n'attend pas la réponse ne sera pas encore arrivée et donc le singleton n'aura aucun résultat d'enregistré à  retourner.
  • Am_MeAm_Me Membre
    septembre 2014 modifié #81

    Ah oui surtout que pas mal d'application donne la possibilité de resfresh. 


    Ah d'accord je pense avoir mieux compris. Bah ça se trouve en y repensant je vais devoir y ajouter un bouton refresh ;D  dans mon appli mais je vais faire en sorte que non pour le moment et travailler en automatique.


     


    Faudra en tant voulu que je test ton raisonnement sur papier. Mais merci pour toutes les explications


  • AliGatorAliGator Membre, Modérateur
    Bah après :
    - Soit tu fais le mode simple et naà¯f, à  chaque fois que l'utilisateur tap sur le bouton Refresh, tu renvoies systématiquement la requête, et appelle la completion quand ladite requête a sa réponse.
    - Soit tu veux éviter de renvoyer la requête si elle est déjà  en cours, pour optimiser, et dans ce cas tu n'envoies la requête que la première fois mais il faut quand même penser à  appeler les 10 completions qui ont été demandées, pour rester logique (10 appels à  la méthode = 10 completions.

    Car sinon si des fois la completion est appelée (requête envoyée) et des fois pas (requête déjà  en cours), parce que tu aurais choisi dans ton implémentation d'appeler juste le completionBlock() dans la completion du NSURLSession, et de ne rien faire du tout (pas même mémoriser le completionBlock) quand la requête est déjà  partie, bah ça ne serait pas cohérent.
    Ou alors si des fois la completion est appelée quand les données sont disponibles (réponse reçue) et des fois la completion est appelée trop tôt (requête déjà  en cours et du coup tu appelles la completion tout de suite au lieu d'attendre la réponse), dans le cas où tu aurais décidé d'appeler la completion immédiatement si la requête est en cours, là  encore ça ne serait pas cohérent.

    Du point de vue de l'extérieur, tu as fait une API qui est sensée appeler la completion systématiquement (et pas seulement si une requête sous-jacente est faite ou pas), et seulement quand les données sont enfin disponibles. Donc faut te tenir à  cette API, car sinon si le comportement est différent (si la completion n'est pas toujours appelée au même moment, voire pas toujours appelée du tout, selon ton implémentation) selon le timing, selon si tu appelles plusieurs fois tes méthodes en un laps de temps trop court ou pas, le comportement risque de ne pas du tout être déterministe.



    En général, quand on débute pour faire ce genre de choses, on ne se prend pas la tête : on envoie systématiquement la requête à  chaque fois, et on appelle le completionBlock correspondant quand la requête est finie. Au risque d'appeler 10x la requête en un laps de temps très court avant même d'avoir reçu une seule réponse, et que les 10 réponses arrivent ensuite plus tard toutes d'un coup, et qu'on ait donc fait + de requêtes réseau qu'il n'en faut. C'est pas le plus optimisé, mais au moins c'est simple à  implémenter.

    Là  vu comment la discussion a dérivé, on est allé plus loin, jusqu'à  l'optimisation aux petits oignons pour éviter des appels réseau inutile. Donc forcément ça rend le code un peu plus complexe, et c'est pas forcément évident à  suivre pour quelqu'un qui n'est pas habitué, ni habitué à  manipuler les blocks, ni à  manipuler les concepts d'asynchronisme ou à  raisonner en prenant en compte les aspects temporels et de concurrence, ni habitué à  faire de l'architecture logicielle et à  penser à  tous les cas tricky qui peuvent intervenir... on est peut-être allé un peu vite en t'apprenant beaucoup de concepts d'un coup, pour lesquels il faut l'habitude et l'expérience pour bien tous les maà®triser, mais bon, au moins tu auras un aperçu des genre de choses jusqu'auxquelles on peut arriver si on pousse l'architecture logicielle assez loin.
  • Oui j'ai le cerveau qui va me lâcher là  haha. Je vais y aller doucement mais essayer d'y aller vite (je me contredis mais je me comprend).


     


    Mais je comprendrais un jour. Merci pour tout en tout cas


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