Architecture : un DataSource asynchrone ?

AliGatorAliGator Membre, Modérateur
05:44 modifié dans API AppKit #1
Bonjour,

Je cherche le meilleur moyen propre (MVC quoi) d'implémenter une sorte de dataSource asynchrone...

Je m'explique : j'ai une vue qui va avoir à  représenter disons plusieurs images. Et je cherche une façon propre de fournir à  cette vue chacune des images à  afficher. Le problème c'est que autant parfois ces images seront directement disponibles (je les aurais dans un NSArray par exemple), autant ces images peuvent aussi parfois ne pas être disponibles tout de suite (par exemple nécessiter un téléchargement).

Donc j'avais bien pensé au principe du dataSource classique, ma vue demande à  son dataSource un truc comme "imageForIndex:" et le dataSource lui renvoie... ça ça marche si mes images sont dispos de suite... mais dans le cas où je voudrais utiliser ma vue avec des images réseau, bof...

Ou alors c'est mon modèle qui télécharge les images, est notifié quand ces dernières sont disponibles, et informe alors la vue avec "setImage:ForIndex:". Mais d'une part ça me gène un peu de voir le flux d'info dans ce sens (modèle qui informe la vue directement avec les données et pas vue qui demande les données au modèle)... et d'autre part dans le cas où j'ai un NSArray d'images statiques déjà  dispo (et non pas à  télécharger) c'est moins propre que la solution dataSource...


Bref, si vous avez des idées pour une architecture propre et qui fasse un compromis entre les 2 cas (images dispo de suite ou plus tard) ?

Réponses

  • 05:44 modifié #2
    J'ai eu à  faire à  peu près la meme chose (si j'ai bien compris ton problème) il y a 1 semaine.
    Si on reprend ton exemple dataSource avec "imageForIndex:", et qu'on considère que ça fonctionne a peur près comme une TableView, lorsque tu envoies [myView setNeedsDisplay] ça devrait rappeler la méthode "imageForIndex:" et recharger uniquement les imageVisibles.
    D'un autre côté, tu aurais un système d'ID. par exemple tu as une Array qui ne contient que des ID et tu t'en sers comme source, d'un autre tu as un NSDictionary avec des NSDictionnary à  chaque ID.
    En considérant qu'au départ aucune images ne sont disponibles, tu vas simplement mettre une image de préchargement qui servira juste à  faire joli tant que l'image n'est pas chargée.

    la méthode DataSource "imageForIndex:" :

    <br />- (NSImage*)imageForIndex:(NSInteger)*index<br />{ <br />&nbsp; &nbsp; NSDictionary* currentIDInfos = [imagesDictionary objectForKey:[masterArray objectAtIndex:index]];<br /><br />&nbsp;  if([currentIDInfos objectForKey:@&quot;Image&quot;]!=nil)<br />&nbsp; &nbsp; &nbsp; &nbsp;  return [currentIDInfos objectForKey:@&quot;Image&quot;];<br />&nbsp;  else<br />&nbsp; &nbsp; &nbsp; &nbsp;  [self downloadImageForIndex:index];<br /><br />&nbsp;  return [NSImage imageNamed:@&quot;Loading&quot;];<br />}<br />
    


    le "downloadImageForIndex:" se chargerait de télécharger l'image et de la mettre dans le "imagesDictionary".
    Bon évidemment rien n'oblige à  utilise un dictionnaire pour stocker les images, on peut se servir de l'index comme ID tout simplement et en faire une simple NSArray.

    Le plus simple je pense c'est carrément de dire que masterArray = [imagesDictionary allKeys];


    Après je suis vraiment pa sûr de ce que je dis et encore moins d'avoir compris si c'est ce que tu voulais... Mais si c'est ça, il est clair qu'il y a sûrement plus propre.
  • AliGatorAliGator Membre, Modérateur
    05:44 modifié #3
    Bah le problème est justement là ... je pense que tu as grosso modo compris mon problème (sauf que je m'embête pas avec tes IDs moi je fais direct avec les indexes) :
    -(UIImage*)imageForIndex:(NSUInteger)index<br />{<br />&nbsp; UIImage* img = [imagesArray objectAtIndex:index];<br />&nbsp; if (img == [NSNull null]) {<br />&nbsp; &nbsp; [self startDownloadingImageForIndex:index];<br />&nbsp; &nbsp; return nil;<br />&nbsp; }<br />&nbsp; return img;<br />}
    
    Où imagesArray est un tableau initialisé avec mes UIImages... avec les emplacements pour lesquels je n'ai pas encore l'image initialisés à  [NSNul null] au lieu d'une vraie image.
    Et ma vue a en interne une UIImage* downloadImage qu'elle affiche quand imageForIndex: retourne nil au lieu d'une vraie UIImage.

    Donc dans ce cas ça marche... sauf que ce qui m'embête un peu c'est que pour faire marcher ça, au fur et à  mesure que mes images sont disponibles (si je les télécharge au fur et à  mesure) et que je les met alors dans mon imagesArray... il faut que je fasse des "reloadData" réguliers pour que ma vue réappelle "imageForIndex:" et prenne en compte les nouvelles images disponibles.

    Or ça m'embête ce truc du reloadData, car ça veux dire que ça rappelle "imageForIndex:" pour TOUS les indexes, même ceux pour lesquels je n'ai pas changé l'image... Or c'est un peu problématique car en réalité ma vue n'utilise pas les UIImages telles qu'elles mais effectue des traitements dessus et garde la version traà®tée en mémoire pour les afficher. Donc un reloadData rechargerait toutes les images et referait le traitement sur TOUTES les images, même celles qui n'ont pas changé entre temps...

    La solution que je vois pour l'instant c'est de garder ce principe de dataSource et de rajouter une méthode "updateImageForIndexes:(NSIndexSet)indexes" que j'appelle avec uniquement les index des images nouvellement chargées et à  mettre à  jour au lieu de tout reloader... Mais est-ce vraiment propre comme façon de faire ?



    Je me demande comment par exemple CoverFlow fait, car quand on regarde la vue CoverFlow d'un dossier, il y a justement parfois un temps de latence avant que ne s'affiche les images de chaque "cover", le temps qu'il calcule l'aperçu du document s'il n'est pas immédiatement disponible...
  • avril 2009 modifié #4
    dans 1239639174:

    Bah le problème est justement là ... je pense que tu as grosso modo compris mon problème (sauf que je m'embête pas avec tes IDs moi je fais direct avec les indexes) :
    -(UIImage*)imageForIndex:(NSUInteger)index<br />{<br />&nbsp; UIImage* img = [imagesArray objectAtIndex:index];<br />&nbsp; if (img == [NSNull null]) {<br />&nbsp; &nbsp; [self startDownloadingImageForIndex:index];<br />&nbsp; &nbsp; return nil;<br />&nbsp; }<br />&nbsp; return img;<br />}
    
    Où imagesArray est un tableau initialisé avec mes UIImages... avec les emplacements pour lesquels je n'ai pas encore l'image initialisés à  [NSNul null] au lieu d'une vraie image.
    Et ma vue a en interne une UIImage* downloadImage qu'elle affiche quand imageForIndex: retourne nil au lieu d'une vraie UIImage.

    Donc dans ce cas ça marche... sauf que ce qui m'embête un peu c'est que pour faire marcher ça, au fur et à  mesure que mes images sont disponibles (si je les télécharge au fur et à  mesure) et que je les met alors dans mon imagesArray... il faut que je fasse des "reloadData" réguliers pour que ma vue réappelle "imageForIndex:" et prenne en compte les nouvelles images disponibles.

    Or ça m'embête ce truc du reloadData, car ça veux dire que ça rappelle "imageForIndex:" pour TOUS les indexes, même ceux pour lesquels je n'ai pas changé l'image... Or c'est un peu problématique car en réalité ma vue n'utilise pas les UIImages telles qu'elles mais effectue des traitements dessus et garde la version traà®tée en mémoire pour les afficher. Donc un reloadData rechargerait toutes les images et referait le traitement sur TOUTES les images, même celles qui n'ont pas changé entre temps...

    La solution que je vois pour l'instant c'est de garder ce principe de dataSource et de rajouter une méthode "updateImageForIndexes:(NSIndexSet)indexes" que j'appelle avec uniquement les index des images nouvellement chargées et à  mettre à  jour au lieu de tout reloader... Mais est-ce vraiment propre comme façon de faire ?



    Je me demande comment par exemple CoverFlow fait, car quand on regarde la vue CoverFlow d'un dossier, il y a justement parfois un temps de latence avant que ne s'affiche les images de chaque "cover", le temps qu'il calcule l'aperçu du document s'il n'est pas immédiatement disponible...


    Justement Ali, je te parle de setNeedsDisplay: qui ne recharge que les row visibles ! C'est pour ça moi aussi j'avais une liste de 5000 items et un reload data c'est super lourd O.O  ducoup setNeedsDisplay: m'a carrément arangé !

    En plus je pense avoir compris que tu développes pour iPhone (t'as posté dans la rubrique Mac :p ), donc limite tu fais comme l'App Store et tu charge les images une fois le scroll terminé, ça évitera que ça soit appelé dès que le mec scroll, parce qu'il suffit qu'il fasse un grand coup de scroll et qu'il descende tout en bas de ta vue et là  je te dis pas tout ce qu'il va charger d'un coup
  • AliGatorAliGator Membre, Modérateur
    05:44 modifié #5
    Le souci c'est que moi ce n'est pas une TableView... mais une classe perso dérivant de NSView (enfin de UIView plus exactement car c'est sur iPhone)...
    Ma UIView contient plusieurs CALayers, chacun ayant donc un contenu que devrait me fournir ma dataSource...

    Alors en effet setNeedsDisplay est dispo pour les CALayer, et ça me permettrait de ne recharger que les CALayers visibles... c'est vrai... Mais il en reste encore beaucoup ! Et surtout comme je l'ai dit, mon dataSource fournit des UIImages... mais une fois que ma vue a reçu ces UIImages, elle fait des traitements dessus, et récupère une CGImageRef pour la mettre comme contenu de mes CALayers...

    Donc même ne redemander que les UIImages de mes CALayers visibles et (pas ceux non visibles) c'est trop car s'il y en a une 10aine de visibles avec 9 qui ont déjà  un contenu et 1 qui n'en avait pas encore mais que la dataSource vient juste de recevoir... Je veux pas recalculer les 9 CGImageRef déjà  traitées et visibles, mais que celle qui n'existait pas !
  • 05:44 modifié #6
    dans 1239640127:

    Le souci c'est que moi ce n'est pas une TableView... mais une classe perso dérivant de NSView (enfin de UIView plus exactement car c'est sur iPhone)...
    Ma UIView contient plusieurs CALayers, chacun ayant donc un contenu que devrait me fournir ma dataSource...

    Alors en effet setNeedsDisplay est dispo pour les CALayer, et ça me permettrait de ne recharger que les CALayers visibles... c'est vrai... Mais il en reste encore beaucoup ! Et surtout comme je l'ai dit, mon dataSource fournit des UIImages... mais une fois que ma vue a reçu ces UIImages, elle fait des traitements dessus, et récupère une CGImageRef pour la mettre comme contenu de mes CALayers...

    Donc même ne redemander que les UIImages de mes CALayers visibles et (pas ceux non visibles) c'est trop car s'il y en a une 10aine de visibles avec 9 qui ont déjà  un contenu et 1 qui n'en avait pas encore mais que la dataSource vient juste de recevoir... Je veux pas recalculer les 9 CGImageRef déjà  traitées et visibles, mais que celle qui n'existait pas !


    Mais justement je vois pas où est le soucis, tu peux meme rajouter un BOOL dans le dictionary à  l'index "x" du genre "isLoading". à  partir du moment où tu lance le téléchargement, isLoading passe à  YES mais image == null. Vu que "isLoading" == YES, tu ne balanceras pas de nouveau téléchargement dans ton data source.
    Lorsque l'image est reçu, tu balances un setNeedsDisplay qui DOIT a tout prix rebalancer "imageForIndex:" et en meme temps tu dois balancer le traitement de l'image obtenue, et là  encore tu peux faire joujou avec un nouveau bool du genre "isWorking".


    Enfin là  je pense pas plus t'aider  :o mais pour moi l'utilisation de bool évitera le re-téléchargement et le-traitement des images.
    Le seul hic, et j'espère que tu le sais, c'est la mémoire.. Faut pas oublier que celle de l'iPhone est super limitée.. C'est pour ça que moi je partais plus sur un dictionary avec des ID histoire de gicler les IDs des items non visibles et ainsi libérer la mémoire. Mais évidemment ça demandera un nouveau téléchargement & traitement de l'image.
  • AliGatorAliGator Membre, Modérateur
    05:44 modifié #7
    C'est là  que je pense pas que tu as pigé mon souci ;)

    Evidemment que je ne vais pas retélécharger l'image, pas fou ! Un simple flag à  la limite suffit en effet.
    Mais le truc c'est que c'est ma classe vue qui fait un traitement de l'image, pas ma classe modèle. Car le traitement est fait pour l'affichage (réduction de l'image, ajout d'un effet, ajout d'un dégradé sur la couche alpha, ce genre de trucs).

    Donc là  un setNeedsDisplay sur ma vue et sur mon CALayer parent va rappeller imageForIndex: pour tous les CALayers visibles (uniquement ceux-là , certes), mais y compris ceux à  qui on a déjà  fourni une image et dont le traitement est déjà  fait.

    Alors je peux dans ma vue mettre aussi un flag pour que setNeedsDisplay n'appelle "imageForIndex:" que pour les CGImageRef de ma vue qui sont encore NULL et pas celles qui ont déjà  été reçues, traitées, puis affectées à  des layers... Mais :
    1) C'est plus du dataSource-like et surtout
    2) Ca veux dire que si mon objet à  l'index N change réellement, et qu'il obtient une nouvelle image mise à  jour... bah là  je peux plus rien faire, je vais demander à  ma vue un setNeedsDisplay mais elle ne va jamais recharger mon image d'index N puisqu'elle en aura déjà  une... même si la nouvelle que je lui propose est différente !
  • 05:44 modifié #8
    Tu t'ai mis dans un sacré bordel  ;D
    Là  je sèche.. à  moins que tu revois carrément tout pour justement que ça fasse ce qu'il faut et que ça le fasse bien. Au risque de devoir tout te retaper  ::)
  • AliGatorAliGator Membre, Modérateur
    05:44 modifié #9
    Bah non justement, rien n'est figé, c'est pour ça que je pose la question, j'en suis encore à  la phase d'architecture du projet.

    Donc pour résumer, j'ai une vue qui affiche plusieurs images, mais :
    1) ces images ne sont pas forcément disponibles dès le début, car peuvent être à  télécharger du réseau
    2) C'est sur iPhone donc attention à  la mémoire consommée

    Le principe est un peu le même que du CoverFlow, où au début la vue n'a pas forcément toutes les images à  disposition, mais au fur et à  mesure que les aperçus des fichiers à  afficher sont disponibles, la vue CoverFlow se met à  jour.

    Pour l'instant la solution la plus simple et la moins consommatrice trouvée c'est que mon modèle appelle "setImage:forIndex:" sur ma vue pour lui donner les images à  mettre à  jour. Mais c'est un peu l'inverse du principe du dataSource, c'est juste ça qui m'embête, c'est que ça fait modèle qui informe la vue et pas vue qui demande au modèle/dataSource.
  • schlumschlum Membre
    05:44 modifié #10
    Le modèle a le droit de notifier les changements à  la vue, ça fait partie du MVC  ;)
  • AliGatorAliGator Membre, Modérateur
    05:44 modifié #11
    Ah bah tu me soulages, schlum ;)

    Bon je crois que je vais garder cette archi alors, mon modèle qui charge ses données tranquillement, et informe la vue au fur et à  mesure qu'il a les données disponibles. Na.
  • schlumschlum Membre
    05:44 modifié #12
    Je trouve ce schéma pas mal... plus clair que celui qu'on peut trouver sur Wikipedia...

    mvc-structure-generic.gif
  • AliGatorAliGator Membre, Modérateur
    05:44 modifié #13
    Ah oui bien ton schéma. Merci schlum.

    Ok donc plutôt que mon modèle envoie un "setImage:forIndex:" ce qui me gênait et n'était finalement pas si MVC que ça, faut plutôt que je prévois une méthode "setNeedsUpdateForIndex:" dans ma vue, que mon modèle appellera quand il aura une nouvelle image de disponible pour cet index... Ce qui aura pour effet que ma vue rappellera la méthode "imageForIndex:" de mon dataSource pour lui redemander la nouvelle image disponible.

    De cette manière, je retrouve le classique modèle "dataSource", et le principe comme quoi c'est bien la vue qui demande les infos au modèle (méhode "imageForIndex:" envoyée au dataSource)... mais il me manquais donc juste le "setNeedsUpdateForIndex:" que mon modèle peut envoyer à  la vue pour lui signaler que le modèle a été mis à  jour sur cette partie du contenu...
    (sur le modèle du "setNeedsDisplay:" en fait, sauf qu'avec ça je lui précise juste ce qui a été modifié sans qu'il ait besoin de tout redemander.)

    Ca me parait pas mal ça comme compromis de solution... En tout cas ça me va bien mieux plutôt que de faire appeler "setImage:forIndex:" par le modèle sur la vue, il me semblais bien que la communication modèle->vue était plutôt du genre évènementielle et que les données elles passaient en général plutôt dans le sens vue qui demande au modèle... C'est ça qui me gênait depuis le début d'ailleurs... mais avec ce principe de "change notification" ça résoud le schmilblick ;)
  • schlumschlum Membre
    05:44 modifié #14
    Oui, c'est tout à  fait ça 
    Un peu comme " setNeedsDisplay " ou " reloadData "... Il ne faut pas envoyer les données.
  • AliGatorAliGator Membre, Modérateur
    05:44 modifié #15
    Cool me disais bien qu'il y avait un truc pas top dans mon archi depuis le début, et c'était bien ça qui me gênait, l'envoi des données dans ce sens... J'aimais pas ça, ça me semblais bof...
    Bon bah cette solution de juste la notification dans ce sens, ça me soulage, là  je trouve ça bien propre et clean, ouf je préfère carrément :)

    Merci schlum  :p :p
Connectez-vous ou Inscrivez-vous pour répondre.