Image asynchrones, cache pour UITableView sans librairie externe...

iosciosc Membre
août 2014 modifié dans API UIKit #1

Bonjour à  toutes et à  tous,

Ceci est mon premier post, alors je tiens à  vous dire déjà  merci de proposer une communauté d'aide francophone si riche.

 

Je me présente, Grégory, je commence depuis peu avec l'objective-c, mais j'apprends très vite.

Mon cas est un peu à  part, car je ne développe pas d'application à  proprement parlé, mais des extensions pour un programme nommé Fusion 2.5 (Clickteam).

 

Ce programme permet aux utilisateurs de créer des applications et des jeux sans lignes de code pour windows, android, ios, etc...

 

Mon loisir à  moi, c'est donc de développer des extensions pour que les utilisateurs puissent les utiliser dans les app iOS. Vous l'aurez compris, moi je met les mains dans le code pour faciliter la vie de ces jeunes (ou moins jeunes) gens.

 

Pourquoi j'explique tout ça, parce qu'entre autre, le SDK de ce programme qui nous permet à  nous, développeurs, de créer ces dites-extensions, ne supporte pas l'import de librairies externes - enfin j'entends par là  qu'il faudrait, pour utiliser ces librairies, que les utilisateurs les importent manuellement dans XCode. Autant dire hors de question, ceci viendrait en effet nuire à  l'esprit même de ce programme.

 

J'en viens donc à  mon problème. Je développe actuellement une extension UITableView qui permettra donc d'inserer l'objet de façon toute simple dans les applications des utilisateurs.

 

Mes cell affichent une image dont l'adresse est stockée dans NSMutableDictionary stocké dans un NSMutableArray.

Les images sont chargées de manière asynchrones et stockées en cache (via un code que j'ai trouvé sur internet, dont j'ai retrouvé les traces dans une question d'un utilisateur ici, et pour laquelle AliGator n'avait pas l'air convaincu.... on verra)

Toute fonctionne à  merveille jusque là , mais seulement voila, lors qu'un [tableView reloaddata] est appelé, une fois sur deux, les images affichées ne sont pas à  la bonne place, et je dois donc scroller afin que la cell ne soit plus visible pour réparer le soucis.

 

J'ai bien compris, le temps que l'image soit chargée, la cell a surement été ré-utilisée et l'affichage de l'image est alors erroné... Le seul moyen que j'ai trouvé, c'est de virer la condition (cell == nil), ou encore un identifier unique par cell, autant sur l'iphone 5s, pas de problème, mais j'ai des gros doutes pour les anciens devices...

 

Dans mes essais, reloaddata est appelé dans une "Action" que l'utilisateur peut utilisé, et qui permet d'éditer le texte d'une cellule - Pour afficher la modification instantanément je dois donc faire usage de reloaddata.

 

J'ai lu un milliard de chose, et souvent on conseille AFNetworking (AliGator...), SDWebImage, etc...

Mais je le répète impossible.

 
Bon j'arrête le pavé, et je met du code (allégé pour l'occasion) - dites moi si vous avez besoin d'autre chose. Un grand merci à  ceux qui pourront m'aidé à  régler le problème, et svp, soyez tolérant pour mon code, je débute :-)

 



- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
NSString *identifier = @cellidentifier;
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:identifier];
if (cell == nil)
{
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:identifier] autorelease];
...
}

imgURL = [[tableViewArray objectAtIndex:indexPath.row] valueForKey:@image];
cell.imageView.image = [UIImage imageNamed:@placeholder.png];

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

dispatch_async(queue, ^{
UIImage* image = [self imageNamed:imgURL cache:YES];

if (image) {
dispatch_async(dispatch_get_main_queue(), ^{
UITableViewCell *updateCell = (id)[tableView cellForRowAtIndexPath:indexPath];
if (updateCell) {
cell.imageView.image = image;
}
});
}
});
...
return cell;
}

pour le cache :



- (UIImage*)imageNamed:(NSString*)imageNamed cache:(BOOL)cache
{
UIImage* retImage = [imageCacheDictionary objectForKey:imageNamed];
if (retImage == nil)
{
retImage = [UIImage imageWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:imageNamed]]];

if (cache && retImage)
{
if (imageCacheDictionary == nil)
imageCacheDictionary = [NSMutableDictionary new];

[imageCacheDictionary setObject:retImage forKey:imageNamed];

} else if (!retImage) {
// si l'url n'abouti à  rien on essaye avec imageNamed (local)
retImage = [UIImage imageNamed:imageNamed];

if (retImage)
{
if (imageCacheDictionary == nil)
imageCacheDictionary = [NSMutableDictionary new];

[imageCacheDictionary setObject:retImage forKey:imageNamed];

// si ni l'url, ni l'imageNamed ne renvoi une image valide, alors on place le placeholder à  la place
} else {
retImage = [UIImage imageNamed:@placeholder.png];

if (imageCacheDictionary == nil)
imageCacheDictionary = [NSMutableDictionary new];

[imageCacheDictionary setObject:retImage forKey:imageNamed];
}

}
}

return retImage;
}

Réponses

  • Hello,


    Pour ton problème de cellules, essai d'utiliser une custom cell ( une cellule qui hérite de UITableViewCell), j'ai déjà  eu un problème comme le tien avec les cellules UITableViewCell et j'ai jamais compris pourquoi.


    Quelques conseils pour ton code :


    NSString *identifier =[@cellidentifier];

    ...


    Tu n'as pas un warning par hasard ici ? Tu es entrain d'assigner un Array a une variable NSString ?


    Pourquoi "updatedCell" ? Tu peux supprimer la ligne qui récupère la cellule, tu assigne juste l'image à  la fin de la requête.


    Pour la gestion du cache, utiles plutôt la classe NSCache au lieux d'un NSMutableDictionary.


    Tu peux aussi rajouter une gestion du cache en disque plutôt que d'utiliser juste un cache mémoire.
  • iosciosc Membre

    Bonjour Samir,


     


    Merci pour ton intervention.


    Pour l'identifier, c'est en collant et enlevant les choses inutiles du code pour le forum que j'ai laissé les [ ] ... désolé et corrigé.


    Ensuite, je préférai vraiment éviter la custom cell, sachant que mon code en fait est complètement fini - j'ai juste cet aspect là  à  régler avant de la release public - Travailler avec une custom cell reviendrait à  perdre des heures de boulot...

  • samirsamir Membre
    août 2014 modifié #4
    Essaie :

    ...

    cell.imageView.image = image;

    [cell setNeedsLayout];


    En mettant aussi le contentMode de imageView de la cellule a aspectFit
  • iosciosc Membre
    août 2014 modifié #5

    Non, toujours pareil, j'avais déjà  essayé.


    Le problème, je pense, intervient quand [tableview reloaddata]; est appelé (dans la méthode correspondante à  l'"Action" Editer de l'extension - pour mon exemple, mais on retrouve reloaddata dans les autres "Actions" telles que ajouter une cellule, etc...)


     


    Edit. J'ai peut-être trouvé quelque chose pour le moment, au lieu de faire un tableview reloaddata, je fais un tableview reloadRowsAtIndexPaths ... 


    Pour le moment plus de problème lors de l'édition, mais qu'en sera t-il lors d'un scrolling lourd, lors d'un pull to refresh...


    Donc la demande d'une meilleure solution roule toujours. 


     


    Edit 2. Hop, même problème avec le pull to refresh, comme pressentie...


  • iosciosc Membre

    Bonjour,


    Je me permet de relancer un peu le sujet - si quelqu'un pouvait m'aider à  régler ceci. Merci !


  • Bonjour,


     


    J'ai eu un problème similaire. Que j'ai résolu tout simplement en ajoutant :



    [cell.imageView setImage:nil];

    en tant que dernière ligne de code de tableView:cellForRowAtIndexPath:


     


    À noter que mon code est légèrement différent du tiens vu que j'utilise des NSBlockOperation que j'ajoute en queue juste avant cette fameuse ligne de code.


  • iosciosc Membre

    Bonjour Kuberman,


    Tu veux dire après return cell; ? 


    Je viens d'essayer, j'ai l'impression que le problème arrive moins souvent, mais ce n'est qu'une impression, et surtout il se produit tout de même encore....


     


    Je commence un peu à  désespérer sur ce truc...




  •  


    Tu veux dire après return cell; ? 


     




     


    Quand même pas non... faut bien que le code s'exécute.

  • denis_13denis_13 Membre
    août 2014 modifié #10

    Bonjour,




    tu peux utiliser NSCache, qui est assez comparable à  un dictionnaire, en utilisant par exemple l'index path de ta cellule comme clef. Il faut ensuite au moment ou les données arrivent vérifier que l'index path de ta cellule fait partit des cellules affichées.


     


     

  • iosciosc Membre
    août 2014 modifié #11


    Quand même pas non... faut bien que le code s'exécute.




     


    De toute manière ça ne résout pas le problème...

  • en utilisant indexPathsForVisibleRows:


  • pour mieux résumer, au lieu de garder l'id de la cellule, au moment où tu reçois l'image, tu la stockes dans ton cache (instance de NSCache), comme tu as gardé l'indexPath, tu vérifie ci celui-ci fait partie des cellules visibles), si c'est le cas tu mets ta cellule à  jour. Au moment initiale ou tu affiche ta cellule tu commences par vérifier si l'image n'est pas déjà  dans ton cache (avec le path qui sert de clef) et lance la recherche de l'image que si tu ne l'as pas trouvée.


  • Bon je vais essayer de réecrire la partie sur l'image avec tes conseils, et je reviens ici ensuite pour donner des nouvelles.

    J'espère juste que cela va régler le problème.

    Merci en tout cas!
  • Je reviens, je n'ai pas eu le temps de réécrire le code - J'ai par tout simplement testé mon code en enlevant la partie 'mise en cache'.


    Le problème est toujours présent - il semble donc que réécrire cette partie là  ne résoudras pas le problème (même si je ne doute pas qu'elle sera plus efficace).


     


    Une solution pour définitivement réglé ça ?


  • bonjour,


    en dehors du fait que j'utilise


     


        [NSURLConnection sendAsynchronousRequest:imageRequest


                                           queue:[NSOperationQueue mainQueue]


                               completionHandler:^(NSURLResponse* response, NSData* listingData, NSError* error)


     

     


    chez moi, la méthode que je t'ai décrite fonctionne vraiment bien...


  • iosciosc Membre
    août 2014 modifié #17

    Super denis_13, avec cette méthode ça fonctionne. Va savoir pourquoi avec GCD ça ne fonctionnait pas....?


    Pour information, mon code mis à  jour (sans mise en cache pour le moment donc) donne :



    - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
    {
    NSString *identifier = @cellidentifier;
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:identifier];
    if (cell == nil)
    {
    cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:identifier] autorelease];
    ...
    }

    cell.imageView.image = [UIImage imageNamed:@placeholder.png];

    NSString *imageURL = [[tableViewArray objectAtIndex:[self getTableViewArrayIndex:indexPath.section row:indexPath.row]] valueForKey:@image]; // getTableViewArrayIndex propre à  la structure de mon code...
    NSURLRequest *imageRequest = [NSURLRequest requestWithURL:[NSURL URLWithString:imageURL]];
    [NSURLConnection sendAsynchronousRequest:imageRequest
    queue:[NSOperationQueue mainQueue]
    completionHandler:^(NSURLResponse *response, NSData *data, NSError *error)
    {
    if (!error)
    {
    if (data)
    {
    UIImage *image = [UIImage imageWithData:data];
    if (image)
    {

    cell.imageView.image = image;
    }
    }
    }
    }];
    ...
    return cell;

    Je reviens ici quand j'aurai réécrit la mise en cache.


  • ce dernier code me parait tout de même suspect car ta cellule a pu être recyclée avant que tu ne récupère ton image. Comme je l'ai écrit plus haut, il vaut mieux que tu te bases sur l'indexpath pour vérifier si ton image correspond bien à  la cellule affichée. 


  • si les urls de tes images sont liées à  l'indexpath, il est très simple de mettre en cache en utilisant un dictionnaire voir mieux une instance de NSCache.


  • iosciosc Membre
    août 2014 modifié #20

    Bon je viens de faire un essai rapide avec NSCache inclus - Mon interrogation maintenant c'est, si une même image est utilisée deux fois ou plus par différentes cellules, alors celle-ci va tout de même être récupérée vu que la KEY du NSCache est l'indexPath et non l'Image.


     


    Comment éviter ça en utilisant ta méthode ?


    Voici le nouveau code : 



    NSCache *imageCache = [NSCache alloc] init];
    ...

    if ([imageCache objectForKey:indexPath] != nil)
    {

    if ([[tableView indexPathsForVisibleRows] containsObject:indexPath])
    {
    NSData *data = [imageCache objectForKey:indexPath];
    if (data)
    {
    UIImage *image = [UIImage imageWithData:data];
    if (image)
    {
    cell.imageView.image = image;
    }
    }
    }

    } else {
    NSURLRequest *imgRequest = [NSURLRequest requestWithURL:[NSURL URLWithString:imageURL]];
    [NSURLConnection sendAsynchronousRequest:imgRequest
    queue:[NSOperationQueue mainQueue]
    completionHandler:^(NSURLResponse *response, NSData *data, NSError *error)
    {
    if (!error)
    {
    if ([[tableView indexPathsForVisibleRows] containsObject:indexPath])
    {
    if (data)
    {
    UIImage *image = [UIImage imageWithData:data];
    if (image)
    {
    [imageCache setObject:data forKey:indexPath]; // mise en cache
    cell.imageView.image = image;
    }
    }
    }
    }
    }];

    }

  • il doit y avoir plein de solutions, tu peux utiliser deux niveau de cache, (deux instances de NSCache) dans le premier tu stock l'url en fonction avec le path comme clef dans le seconde l'image avec l'url en clef ...


    Cela ne faut le coût que si les dupliquât sont nombreux 


  • par ailleurs dans ton code, tu devrais mettre ton image en cache même si la cellule n'est pas visible, ca ira plus vite




  • Cela ne faut le coût que si les dupliquât sont nombreux 




     


    Oui mais je ne peux contrôler cet aspect, sachant que ce code est pour la création d'une extension que des centaines de personnes vont utiliser de cent manières différentes. Je suis obligé d'envisager cette possibilité.


     


     




    par ailleurs dans ton code, tu devrais mettre ton image en cache même si la cellule n'est pas visible, ca ira plus vite




     


    Je vais faire ça. Merci à  toi denis_13 !!

  • iosciosc Membre
    août 2014 modifié #24

    Re - J'essaye et réessaye mais je n'arrive pas à  mettre en place ce double-cache.


    En résumé, je cache de cette manière là  :



    [imageCache setObject:imageURL forKey:indexPath];
    [imageSubCache setObject:data forKey:imageURL];

    Mais je n'arrive pas à  trouver les bonnes conditions pour retrouver correctement l'image placée en cache.


    Pourrais-tu me donner un coup de main sur le code ?


     


    Pour info, j'essaye de récuperer l'image de la sorte :



    NSData *data = [imageSubCache objectForKey:[imageCache objectForKey:indexPath]];
  • à  première vu cela me semble bien (à  part le nom, tu pourrais appeler le premier urlsCache ?), as tu vérifié que les caches ne sont pas nil ?

  • j'ai un peut tendance à  lire les choses en diagonale, pour les gens comme moi c'est bien d'avoir des noms assez explicites car autrement il m'arrive de ne pas comprendre ce que je lis... par exemple à  la place de d'imageCache j'aurais mis imageUrlCache et à  la place d'imageSubCache, probablement imageDataCache, même si on ne décrit que les valeurs c'est déjà  ca...


    Tu peux faire un log pour chaque étape, mais ce serait bien que tu écrives des tests unitaires de manière à  voir si tu récupère bien de que tu cherches, ca te contraindra a avoir des méthodes explicites si tu décide de changer ta méthode de cache dans le future ce sera plus facile, autrement dit d'avoir une méthode - (NSImage *)imageForIndexPath:(NSIndexPath *)anIndexPath, plutôt que de l'insérer au milieux de ton code.


  • iosciosc Membre
    août 2014 modifié #27

    Bon pour le moment je n'ai pas résolu le soucis.


    Cependant, je viens de me rendre compte que dès qu'une cellule est déplacée/supprimée, l'image n'est pas déplacée/supprimée.


    Cela viens assurément du fait que l'image est stockée dans NSCache via l'indexPath de la cellule - mais alors comment éviter ce second problème alors ?


     


    Edit. Bon en fait il me suffit de déplacer l'objet de NSCache quand la cellule est déplacée, vider le NSCache lorsque la cellule est supprimée. Je retourne au premier problème.


  • iosciosc Membre
    août 2014 modifié #28

    Pour ce qui est de faire une méthode extérieure, je comptais bien le faire, mais pour le moment j'essaye de régler le problème avec ce double-cache. Pareil pour la dénomination.


    Je bloque totalement, je ne vois toujours pas ce que je fais de mal donc voici le code, en espérant que ca permette de résoudre le problème.



    if ([imageCache objectForKey:indexPath] != nil)
    {
    if ([imageSubCache objectForKey:imageURL] != nil)
    {
    if ([[tableView indexPathsForVisibleRows] containsObject:indexPath])
    {
    NSData *data = [imageSubCache objectForKey:[imageCache objectForKey:indexPath]];
    if (data)
    {
    UIImage *image = [UIImage imageWithData:data];
    if (image)
    {
    cell.imageView.image = image;
    }
    }
    }
    }

    } else {

    NSURLRequest *imgRequest = [NSURLRequest requestWithURL:[NSURL URLWithString:imgURL]];
    [NSURLConnection sendAsynchronousRequest:imgRequest
    queue:[NSOperationQueue mainQueue]
    completionHandler:^(NSURLResponse *response, NSData *data, NSError *error)
    {
    if (!error)
    {
    if ([[tableView indexPathsForVisibleRows] containsObject:indexPath])
    {
    if (data)
    {
    UIImage *image = [UIImage imageWithData:data];
    if (image)
    {
    [imageCache setObject:imageURL forKey:indexPath];
    [imageSubCache setObject:data forKey:imageURL];
    cell.imageView.image = image;
    }
    }
    }
    }
    }];

    }

  • ca m'a l'air un peu dans le désordre, tu n'as pas à  vérifier si ta cellule et visible dans ta première condition, je vais regarder de plus prêt ...


  • ton cache devrait être une variable d'instance de ton contrôleur, pas local à  ta méthode (donc plutôt [self imageCache] ) où est comment est il initialisé ?

  • dans ton cas tu n'as pas réellement besoin de double cache puisqu'au départ tu as déjà  un tableau avec les urls, donc tu peux récupérer (je n'avais pas lu suffisamment attentivement tes premiers posts), voici une suggestion...


     


    - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath


    {


        


        NSString *identifier = @cellidentifier;


        UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:identifier];


        


        //    NSURL *cellImageURL = [[self imageURLCache] objectForKey:indexPath];


        //    UIImage *cellImage = nil;


        //


        //    if (cellImageURL) {


        //        cellImage = [[self imageCache] objectForKey:cellImageURL];


        //    }


        


        NSURL *imgURL = [self imageURLForIndexPath:indexPath];


        


        UIImage *cellImage = [[self imageCache] objectForKey:imgURL];


        


        


        if (cellImage) {


            cell.imageView.image = cellImage;


        } else {


            cell.imageView.image = [self loadingImage];


        }


        


        


        


        NSURLRequest *imgRequest = [NSURLRequest requestWithURL:imgURL];


        


        [NSURLConnection sendAsynchronousRequest: imgRequest


                                           queue:[NSOperationQueue mainQueue]


                               completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) {


                                   if (connectionError || !data) {


                                       cell.imageView.image = [self noImage];


                                       return;


                                   }


                                   UIImage *newImage = [[UIImage alloc] initWithData:data];


                                   [[self imageCache] setObject:newImage forKey:imgURL];


                                   if ([[tableView indexPathsForVisibleRows] containsObject:indexPath]) {


                                       cell.imageView.image = newImage;


                                   }


                                   


                               }];


        


        


        return cell;


    }

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