Ecran loading

CeetixCeetix Membre
03:40 modifié dans API UIKit #1
Salut tout le monde.


Voilà  j'ai une app qui s'occupe de télécharger des données sur un serveur. Je mets donc une view de chargement pour faire patienter pendant que le tout soit télécharger.
C'est un json que je parse et qui contient donc du texte mais aussi des urls d'image. Je fais donc appel à  dataWithContentsOfURL pour télécharger les images. A la fin de ma méthode de parsing de je fais disparaitre mon écran de chargement.


Seul souci, celui-ci disparait avant que les images soient téléchargées car en gros mon UI apparait (bloquée bien entendu) et faut attendre pour ça se débloque  (puisque que tout est fait en thread principal).
Je comprends donc pas pourquoi ma vue de chargement disparait alors que j'ai pas fini de télécharger mes image, vu que tout est sur le thread principal.


Donc je pige pas trop là  ...  ???

Réponses

  • CeetixCeetix Membre
    03:40 modifié #2
    Bon après recompilation et tout le tralala il semble que ça fonctionne maintenant. Enfin si vous pouviez me dire si j'ai correct dans mon raisonnement ce serait cool :)
  • muqaddarmuqaddar Administrateur
    03:40 modifié #3
    Ton download ou parsing doit être sur un thread secondaire en asynchrone avec les méthodes d'Apple non ?
  • AliGatorAliGator Membre, Modérateur
    janvier 2011 modifié #4
    Si bien sûr, ça fait même partie des règles de base.
    Faire du métier et en particulier du download ou du parsing dans le thread principal, c'est aller droit dans le mur.
    C'est logique, ton download bloque la RunLoop et l'update de l'UI (les méthodes de mise à  jour de l'UI n'ont jamais été temps réel)
    Je fais donc appel à  dataWithContentsOfURL pour télécharger les images.
    Beurk. Cherche pas plus loin.
  • CeetixCeetix Membre
    03:40 modifié #5
    Tu utiliserais quoi toi alors ?
    NSURLConnection ?
  • AliGatorAliGator Membre, Modérateur
    janvier 2011 modifié #6
    Bah oui évidemment.
    C'est la première chose à  utiliser dès qu'on fait du download ! Faire les choses de ce type de manière synchrone est LA chose à  éviter.
    La bible pour savoir faire des requêtes réseau en ObjC
  • CeetixCeetix Membre
    03:40 modifié #7
    A éviter la manière asynchrone, c'est pas le contraire ce que tu voulais dire?
    Et est-ce que l'utilisation de NSOperation peut être judicieux dans mon cas ?
  • AliGatorAliGator Membre, Modérateur
    03:40 modifié #8
    Oui bien sûr c'était pour voir si tu suivais :P (j'ai corrigé mon post :D)
  • CeetixCeetix Membre
    03:40 modifié #9
    Je suis très attentif Prof Ali !  :P
    Sinon pour NSOperation tu en penses quoi ? Je m'en suis jamais servi ... :o
  • AliGatorAliGator Membre, Modérateur
    03:40 modifié #10
    Perso pour NSOperation je m'en suis très rarement servi (pour pas dire quasi jamais) avant...

    Mais depuis que GCD est là , au contraire je m'en sers assez souvent. D'autant qu'avant GCD et les blocks, pour utiliser une NSOperation fallait sous-classer NSOperation pour surcharger sa méthode "main" et y mettre le code dedans... un peu fastidieux (et pas vraiment différent de l'autre option de créer une sous-classe de NSThread dédiée à  l'exécution asynchrone) donc bon j'utilisais jamais.
    Mais depuis que GCD et les blocks existent, c'est totalement différent ça change tout : à  l'usage c'est bien plus facile à  utiliser :
    [opQueue addOperationWithBlock:^(void) {<br />&nbsp;  ... ton code à  exécuter de façon asynchrone ...<br />}];
    
    Et hop ayé le code est exécuté par une NSOperation sur une NSOperationQueue dédiée, autrement dit dans un autre thread géré par GCD, t'as rien besoin de gérer. Plus fort encore, comme c'est un block et que le compilateur sait automatiquement avec les blocks référencer les variables extérieures au block mais qui sont utilisées à  l'intérieur du block, gérant même tout seul le retain de l'objet qu'il référence et son release quand le block est fini... Bah du coup si tu as besoin d'utiliser dans ta NSOperation des variables qui sont définies au moment où tu veux lancer le code, t'as rien à  faire. Alors qu'avant fallait prévoir dans la sous-classe de NSOperation des accesseurs pour pouvoir ensuite passer des variables du code principal à  la NSOperation pour qu'elle puisse les utiliser, en pensant à  faire un retain dessus, etc..., là  plus besoin avec les blocks :
    int val = 5;<br />NSString* str = myTextField.text;<br />...<br />[opQueue addOperationWithBlock:^(void) {<br />&nbsp; ... code asynchrone ...<br />&nbsp; ...<br />&nbsp; // là  on utilise str et val sans avoir besoin de les passer explicitement via un accesseur ou quoi<br />&nbsp; // (ce qu&#39;on aurait à  faire avec un NSThread ou avec les NSOperation avant GCD et les blocks)<br />&nbsp; NSString* x = [NSString stringWithFormat:@&quot;Utilisation de str=%@ et val=%d !&quot;,str,val];<br />&nbsp; ...<br />&nbsp; ... fin du code asynchrone ...<br />}];
    
    Bref, depuis GCD au contraire les blocks c'est très pratique et permet de faire du code asynchrone et des opérations en tâche de fond sans se prendre la tête avec les considérations sur les threads & co, et ça c'est super pratique !


    Maintenant c'est pas pour ça que j'utilise les NSOperation à  tous les coins de rue dans mon code bien sûr. S'il y a déjà  des méthodes prévues pour gérer les choses de manière asynchrone, comme c'est le cas avec NSURLConnection, ou des mécanismes de delegate & co, autant utiliser ce qui existe déjà  pour faire les choses de façon asynchrone !
  • CeetixCeetix Membre
    03:40 modifié #11
    Super Ali ton explication ! Je comprends mieux la puissance de GCD et des blocks ! Je vais tester ça :)
  • CeetixCeetix Membre
    03:40 modifié #12
    Ah et tu si j'ai un code qui contient du NSURLConnection (donc de l'asychrone) ça se fait d'utiliser GCD ?
  • AliGatorAliGator Membre, Modérateur
    janvier 2011 modifié #13
    Bah à  la limite oui je pense que c'est déjà  le cas en interne, les NSURLConnection faisant leurs opérations en tâche de fond (et toi tu utilises les delegate pour gérer les événements asynchrones), derrière soit c'est un NSThread classique, soit c'est GCD. Donc bon.

    ---

    Par contre perso j'ai fait une classe "NSURLLoader" qui utilise non pas GCD (ça utilise NSURLConnection et son mécanisme de delegate en interne, donc pas de NSOperation ni d'appel explicite à  GCD) mais par contre qui utilise les blocks pour rendre plus simple l'utilisation des NSURLRequests (cf exemple plus bas)
    Bon par contre c'est dispo qu'à  partir de OSX 10.6 ou iOS4, les blocks n'existant pas avant

    Je mettrais sans doute ma classe en question sur mon github un jour, avec la doc et tout, mais en gros pour te donner une idée voilà  comment elle s'utilise :
    NSURL* url = [NSURL URLWithString:urlField.text];<br />NSURLRequest* req = [NSURLRequest requestWithURL:url];<br /><br />outputTextView.text = @&quot;Loading...&quot;;<br />NSLog(@&quot;Starting request for %@...&quot;,url);<br /><br />[OHURLLoader URLLoaderWithRequest:req completion:^(OHURLLoader *loader) {<br />	// httpStatusCode = 200 si OK, 404 si not found, etc<br />	NSLog(@&quot;Download of %@ done. HTTP Status code: %d&quot; , url, loader.httpStatusCode); <br />	// loader.receivedString = NSString construite à  partir de loader.receivedData<br />	// en utilisant le NSStringEncoding correspondant à  loader.response.textEncodingName<br />	outputTextView.text = loader.receivedString; // afficher le texte téléchargé<br />} errorHandler:^(NSError *error) {<br />	// Erreur (pas de réseau, url invalide genre avec des caractères interdits, etc)<br />	NSLog(@&quot;Download error of url %@: %@&quot;,url,error);<br />	outputTextView.text = [error localizedDescription];<br />}];
    
    J'ai une autre variante qui permet de préciser un bout de code (block) à  exécuter au fur et à  mesure qu'on reçoit des données, par exemple pour mettre à  jour une barre de progression de ton interface, etc.

    Ca te donne une idée des choses sympa qu'on peut faire avec les blocks. Du coup j'ai plus besoin d'implémenter [tt]connection:didReceiveResponse:[/tt], [tt]connection:didReceiveData:[/tt], [tt]connectionDidFinishLoading[/tt] et tout, ni besoin de dispatcher mon code dans des méthodes différentes un peu partout dans mon .m, ni besoin de mettre par exemple url dans une variable d'instance dans le .h juste pour pouvoir y accéder dans le [tt]connectionDidFinishLoading[/tt] si je voulais l'utiliser, etc, etc. Que du bon donc !
  • CeetixCeetix Membre
    03:40 modifié #14
    Ok bah je confirme c'est super puissant associé au system de block ! J'adore. Du coup je suis resté sur mon dataWithContentsOfURL. Je me suis basé sur l'exemple SeismicXML d'Apple. Ca marche du tonnerre ! Merci Ali ;)
  • AliGatorAliGator Membre, Modérateur
    03:40 modifié #15
    Heu si t'es resté sur dataWithContentsOfURL tu fais un gros #fail là ... c'est justement une méthode synchrone qu'il faut éviter (le seul cas dans lequel ça pourrait être toléré c'est si tu mets ça dans une NSOperation, mais c'est clairement pas la meilleure façon de faire)... donc je suis en train de me dire que t'as pas retenu grand chose de ce sujet si ? ^^
  • CeetixCeetix Membre
    03:40 modifié #16
    Bah je sais pas je l'ai mis en NSOperation ^^. Là  je suis un peu coincé :/
  • CeetixCeetix Membre
    03:40 modifié #17
    Genre j'ai ça pour mon parsing
    <br />NSArray *noeuds = (NSArray *)[feed valueForKey:@&quot;artiste&quot;];<br />		<br />		for (int i = 0; i &lt; noeuds.count; i++) <br />		{<br />			NSDictionary* feuille = (NSDictionary*)[noeuds objectAtIndex:i];<br />			Artiste *unArtiste = (Artiste *)[NSEntityDescription insertNewObjectForEntityForName:@&quot;Artiste&quot; inManagedObjectContext:[self context]];<br />			[unArtiste setNom:[feuille objectForKey:@&quot;nom&quot;]];<br />			[unArtiste setBiographie:[feuille objectForKey:@&quot;description&quot;]];<br />			UIImage *photo = [[UIImage alloc] initWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:[feuille objectForKey:@&quot;photo&quot;]]]];<br />			<br />			[unArtiste setPhoto:UIImageJPEGRepresentation(photo, 100)];<br />			[context save:nil];<br />			<br />			[photo release];<br />			<br />		}<br />
    


    Et je vois pas trop comment faire car si je fais en asynchrone ma boucle va continuer de tourner et mon image sera pas sauvegarder si ? Je sais pas si tu vois ce que je veux dire? Je soulève un problème imaginaire ..
  • AliGatorAliGator Membre, Modérateur
    janvier 2011 modifié #18
    Bon reprenons, en espérant que cette fois à  la fin de l'explication tu n'aies pas compris de travers et nous ponde un truc bizarre.

    Alors d'une part, c'est pas une bonne chose que de charger la UIImage dans le modèle dans ce cas particulier, mieux vaut ne garder que l'URL dans le modèle, éventuellement garder un pointeur vers une UIImage mais nil au début, et charger l'image que quand elle a besoin d'être affichée. Comme ça tu ne charges pas toutes les images si elles ne sont pas affichées. (Du coup tu peux utiliser les méthodes de delegate de scrollview etc. pour savoir quand charger l'image.

    Au final quand tu parses ton XML tu affectes uniquement unArtiste.url, et quand tu vois que tu vas avoir besoin de l'image, tu regardes si unArtiste.image est nil ou pas, s'il est nil tu charges l'image de façon asynchrone et une fois l'image chargée tu la stockes dans unArtiste.image (pour pas avoir à  la recharger la prochaine fois).
    L'idéal c'est même de se mettre à  l'écoute ensuite de la NSNotification "UIApplicationDidReceiveMemoryWarningNotification" et lorsqu'on reçoit la notification, relâcher l'image (et remettre unArtiste.image à  nil) pour libérer un peu la mémoire. Tu pourras toujours recharger l'image d'après l'URL plus tard si tu en as besoin.

    ---

    Mais bon, pour revenir au download même de l'image, il suffit d'implémenter les méthodes de delegate de NSURLConnection dans ta classe Artiste tout simplement (si ton architecture MVC est bien faite et ton modèle proprement pensé ça ne pose aucun problème). Tu prévois dans ta classe "Artiste" une méthode "loadImage" par exemple, qui va créer un NSURLConnection, lancer la connexion asynchrone des données en utilisant self.url, et quand le download est fini (connectionDidFinishLoading:) là  tu peux construire ton UIImage à  partir des NSData reçues et l'affecter à  self.image et basta !

    C'est 3x rien à  faire, y'a que du copier/coller les bouts de code déjà  rédigés dans le URL Loading Programming Guide en plus !
  • cyranocyrano Membre
    03:40 modifié #19
    mmmm..

    dans le cas des blocks + GCD, il faut quand meme synchroniser les variables partagées, ou mieux faire des structs lockfree et c'est la veritable difficulté du multithread.
  • CeetixCeetix Membre
    03:40 modifié #20
    Ah ok bah je pensais pas que l'on pouvais modifier un model Core Data. J'ai juste à  rajouter mes méthodes de download et de delegate et c'est tout ? Je pose la question comme c'est Core Data ...
  • AliGatorAliGator Membre, Modérateur
    03:40 modifié #21
    Ah, je sais pas j'ai jamais utilisé CoreData, j'avais pas fait gaffe à  ce point. Mais bon t'as pas un fichier Article.m qui définis la classe Article comme une sous classe de NSManagedObject, et à  laquelle tu pourrais rajouter ton code de NSURLConnection ?
  • CeetixCeetix Membre
    03:40 modifié #22
    Je vais essayer ça avec les Catégories mais je sais pas si on peut faire appelle à  du délégué si ?
  • AliGatorAliGator Membre, Modérateur
    03:40 modifié #23
    ? Qui a parlé de catégories ?
  • CeetixCeetix Membre
    03:40 modifié #24
    Bah j'ai regardé un peu et pour rajouter des méthodes dans une classe Core Data il faut passer par les catégories apparemment .
  • AliGatorAliGator Membre, Modérateur
    03:40 modifié #25
    Mais la classe "Article", qui c'est qui l'a créée ? T'as pas un fichier Article.m et Article.h ?

    (Et puis sinon si tu n'as pas tu ne peux pas prévoir des classes modèle dédiées pour tout ce qui est partie fonctionnelle de ton MVC ?)
  • CeetixCeetix Membre
    03:40 modifié #26
    C'est xcode qui l'a créé.
    Et donc il faudrait une classe qui représente juste mon entité et ensuite une autre classe juste pour manipuler un objet de cette entité et en gros je ne passerai ensuite que par cette dernière classe ?
  • CéroceCéroce Membre, Modérateur
    03:40 modifié #27
    dans 1294759819:

    Bah j'ai regardé un peu et pour rajouter des méthodes dans une classe Core Data il faut passer par les catégories apparemment .


    Non. Crée une sous-classe de NSManagedObject. Dans le Modèle, il faut aussi spécifier le nom de la classe associée à  l'entité, pour que Core Data sache quel type d'objet instancier. Regarde les sessions de WWDC 2010 consacrées à  Core Data, ça te fera gagner du temps (session 118 "Mastering Core Data").
  • CeetixCeetix Membre
    03:40 modifié #28
    Yep merci Céroce !
Connectez-vous ou Inscrivez-vous pour répondre.