[Résolu] UIImageView/UIButton dans Custom TableViewCell (saccade)
Bonjour,
Voilà j'ai un problème sur mon application.
Je charge des informations d'une page web (en .xml) et je rempli une UITableView (contenant une UITableViewCell custom)
Voilà les information format texte se charge bien. Le problème survient au chargement des images. Quand j'ai 4 UITableViewCell à créer ça marche bien mais au delà genre 18 :
1) Ca saccade
2) Seul mes premières cell charge leur image correspondante et quand je scroll les autres "copie" mes précédentes images déjà chargées.
Je ne comprend pas pourquoi ...
Si qqun pouvait m'éclaircir.
Pour le moment je raisonne comme ça grossomodo (et je fait [tableView reloadData] dans la thread avec dispatch_get_main_queue).
- (UITableViewCell*)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
Et dans cette méthode je fais le chargement de l'image de la cell.
S'il y a besoin de code il n'y a qu'a demander. Merci d'avance
Réponses
On en a déjà parlé maintes et maintes fois sur ce forum, en expliquant le pourquoi du comment, et les différentes méthodes pour traiter ce genre de cas correctement. La plus simple étant de passer par AFNetworking pour toutes tes requêtes réseau car ce framework de toute façon simplifie la vie pour pas mal de trucs de ce côté là , et d'en profiter pour utiliser la catégorie UIImageView+AFNetworking et sa méthode setImageWithURL: qui permet de prendre en compte tout ce mécanisme et de fonctionner même dans des cas de recyclage de cellule de tableview.
Je m'en doute que ce problème fut mainte fois résolu mais je n'avais pas les mots-clés pour chercher le problème d'ou mon post.
Quand tu parles de toutes type de requête : même pour le chargement d'un XML (petit & grand) permettant la récupération de texte ?
Du coup si je comprend bien ça change un peu le code de ce côté la. Car je résonne avec NSUrlSession. Du coup je dois remplacer mes NSURLSessionDataTask par des AFURLSessionManager
Exemple provenant de http://code.tutsplus.com/tutorials/working-with-nsurlsession-afnetworking-20--mobile-22651
C'est vrai qu'AFNetworking était très utile à l'époque où NSURLSession n'existait pas, facilitant bien des choses... mais qu'avec tout ce qu'apporte NSURLSession, AFN est moins "absolument indispensable" maintenant. Sauf pour 2-3 trucs sympas (multipart, cache, ...) mais bon.
Sinon si tu ne veux pas utiliser imageWithURL: de AFNetworking tu peux juste reprendre le principe. Le mieux étant de ne pas faire le chargement des images dans cellForRowAtIndexPath:, en tout cas pas de façon bloquante et synchrone d'une part (surtout pas), et en vérifiant lorsque l'image est enfin téléchargée que l'URL à afficher n'a pas changée entre temps surtout, du fait du mécanisme de recyclage des cellules.
Tu cherches "image tableview" dans les forums ou sur Google tu verras tu as pléthore de messages sur le sujet (dont le fameux sample Apple dédié à cette problématique, mais pour lequel leur approche avec des NSOperationQueue certes marche, mais commence à dater un peu et est un peu complexe par rapport aux outils qu'on a maintenant)
Oups je viens d'installer AFNetworking avec Pods donc je suis perdu la .
DU coup tu me conseil quoi ?
Et je ne sais pas quoi faire dans le bloc du dessus car j'avais l'habitude de récupérer une NSDAta avec le bloc NSUrlSession la j'ai une NSURLResponse.
Donc NSUrlSession peut gérer ça aussi ?
Bon je me lance dans google tout d'abord
J'ai même essayé cela (dans mon bloc NSUrlSession) :
Mais ça ne donne rien de nouveau : toujours le même souci. Je creuse
MAJ : Le problème étant que je fait ma requête dans cellForRowAtIndexPath. (comme tu m'a dis Aligator)
Bon la nuit porte conseil comme on dit je vais revoir ma méthodologie
SDWebImage est également répandu pour fixer les images des UIImageViews de façon asynchrone (cherche son pod).
ça évite d'utiliser le char d'assaut AFNetworking rien que pour ça.
OK Céroce, merci je vais me pencher dessus.
Du coup on peut mettre du code dans cellForRowAtIndexPath avec SDWebImage ah ce que je comprends d'après leur github
1) Mais dans AFNetworking et SDWebImage ils utilisent le placeHolder (en même temps faudrait que je lise la doc). Et je ne comprend pas à quoi il correspond. En théorie si je ne dis pas de bêtise le placeholder en HTML c'est pour mettre un texte exemple dans une "textfield" d'un formulaire (genre e-mail : toto@gmail.com).
2) Et mes autres requêtes qui récupère du texte : NSUrlSession suffit à ce que je comprend du coup AFNetworking j'en ai pas trop besoin pour ca ?
3) Du coup vous me conseillez quoi : me lancez dans AFNetworking : ou laisser tomber car NSUrlSession est à la rescousse. Ou de me lancer dans SDWebImage ?
Bah oui pour une image c'est le même principe que pour l'exemple que tu prends (texte exemple dans un textfield = texte à afficher en attendant que l'utilisateur tape du vrai texte).
Le placeholder dans le cas de SDImage (ou de AFNetworking) c'est l'image à afficher en attendant que la vraie image soit disponible. Puisque naturellement, vu que tu demandes une image venant d'une URL donc du web, le temps qu'elle se télécharge du réseau, tu n'auras pas tout de suite la vraie image de dispo, donc il est plus sympa si tu le souhaites d'afficher à l'utilisateur une image placeholder en attendant (plutôt que de laisser une imageView vide).
Ok. Car j'avais essayé le placeHolder avec AFNetworking mais bon ça ne faisait pas des lueurs au final. J'ai du mal spécifier un truc.
Ensuite moi j'utilise NSURLSessionDataTask. Est-ce que le fait d'utiliser NSUrlSessionDownloadTask peut être plus utile pour charger soit mes images soit mon texte ?
D'après https://speakerdeck.com/chrisfsampaio/afnetworking-2-dot-0-plus-nsurlsession
NSURLSessionDownloadTask est fait pour télécharger des fichiers sur le disque. Du coup c'est un téléchargement comme quand tu télécharges une image sur ton disque dur depuis ton mac ou quoi (et du coup ça peut faire du téléchargement même quand l'appli n'est pas lancée, en laissant iOS télécharger le fichier et l'écrire sur le disque).
Je ne pense pas que ce soit ce que tu veux ? Toi tu veux récupérer des NSData à la fin, directement les données reçues par ta requête (pour les transformer en UIImage avec "imageWithData:"), non ? Et non pas télécharger les fichiers image sur ton disque tout ça pour relire le fichier après dans une UIImage ?
Donc NSURLSessionDataTask est à priori + adapté à ton besoin, permettant d'envoyer une requête et de récupérer la réponse directement dans un NSData dans le block.
Dans un autre message, je vois que tu fais du POST multipart à la mano, en composant toi-même le body.
Si tu n'utilises ni le chargement asynchrone des images, ni le POST d'AFNetworking, il perd beaucoup d'intérêt, comme le disais Ali.
Personnellement, je trouve qu'AFNetworking fonctionne très bien, mais est trop complexe pour des choses simples.
Alors au niveau POST/GET je ne suis pas top top. De quel message parles-tu (le body à la mano)?
Bah pour les images il y a ce que m'a dit Ali : le imageWithURL: ? si ce n'est que ça c'est pas si difficile au final non ? Ou je n'ai rien compris au message d'Ali
Bon je reviens avec un compte-rendu du AFNetworking et sa méthode imageWithURL:
Ca marche beaucoup mieux ! merci à tous d'ailleurs. Je n'ai plus trop le problème de recyclage : je ne l'ai même plus : du moins pour le moment
Du coup SDWebImage je n'ai l'ai pas utilisé pour ce coup-ci. mais je retient le nom
Maintenant que je me gratte la tête : Je n'y suis pas encore arrivé à la mais dans mon application je devrai charger des XML très très grand : est-ce que NSURLSession fera son travail ou AFNetworking est à la rescousse ce coup-ci : comme j'ai lu sur pas mal de site aussi que NSUrlSession+AFNetworking c'est le mariage parfait .
Céroce a confondu avec Kirax et sa question sur le POST multipart ici : http://forum.cocoacafe.fr/topic/12852-envoie-de-fichier-en-file-avec-variable-post/
Ah ok. Je me disais bien que je n'étais pas fou .
Du coup si quelqu'un a des conseil à me donner je suis preneur : on commence tous par être débutant dans AFNetworking ou NSUrlSession
Ali : du coup en utilisant AFNetworking dans cellForRowAtIndexPath ca marche à merveille même trop bien.
Et quand je scroll aucun problème.
Mais en même temps je voudrais faire quelques chose de propre et dire
Mais bon j'ai pas l'impression qu'on puisse faire ça : j'ai même rajouté un attribut à ma class CustomCell : BOOL Loaded
Mais bon vu le problème du recyclage : ça fait n'importe quoi.
Du coup sans "optimisation" ca marche très bien ca me charge l'image dans la bonne cellule : mais quand je scroll ca les charge encore je crois bien (mais c'est transparent à l'oeil nu je ne voyait pas de "rechargement").
Voilà est-ce possible de faire ce que je voulais ? ou pas.
J'avais pensé à changer le tag de la UIImageView dans la méthode setImageWithURL mais ai-je le droit de toucher à leur api ... ?
Bon j'ai trouvé un moyen plus simple. Sauvegarder les images en local dans un repertoire Cache/ et basta.
Après je donnerait la possibilité à l'utilisateur d'effacer le cache
Je vais regarder s'il existe déjà un fichier de cache pour une application : même si je suppose que oui.
Algo : (rien de complexe) au final
C'est aussi pour ça qu'on a tendance à conseiller AFNetworking : c'est parce qu'il intègre tous ces petits détails qui rendent la vie plus facile concernant tout ce qui tourne autour du réseau.
Après, c'est une lib conséquente, et si tu fais toutes tes autres requêtes avec NSURLSession plutôt qu'avec AFNetworking, du coup tu n'utiliseras qu'une partie d'AFN et c'est un peu too much d'importer toute la lib pour juste une fonctionnalité. Si tu ne veux que cette catégorie, mais que pour le reste de ton appli tu préfères utiliser NSURLSession (car après tout maintenant il répond à la plupart des besoins), tu peux en effet préférer SDWebImage, et du coup il faut vérifier qu'il gère lui aussi un cache interne comme le fait AFNetworking et sa catégorie sur UIImageView.
De toute façon si c'est pas le cas c'est pas bien compliqué à implémenter comme algo. Le mieux étant d'utiliser les classes NSCache ou même NSURLCache de Cocoa, qui sont justement faites pour ça.
Ok d'accord : ouai NSUrlSession pour le reste je pense.
J'ai un problème qui persiste dans le recyclage des TableViewCell. J'ai associé un bouton à chaque cell et à chaque bouton j'associe une fonction
.
Mais quand je clique par exemple sur le bouton de la derniere cellule tout en bas : il se trouve que d'autre bouton aussi ce sont vu actionner l'action : mais ils ont le même tag alors qu'il ne devrait pas.
PS : Dois-je ouvrir un nouveau Post ou le laisser ici vu que c'est un peu le même principe ?
Il ne faut ajouter ton action que quand la cellule est créée (dans l'init de sa sous-classe par exemple, ou via le Storyboard, etc) et pas à chaque recyclage.
J'ai l'impression que tu ne maà®trises pas bien le concept / fonctionnement de ce mécanisme de recyclage de cellules d'une tableView. Je t'invite fortement à aller (re-)lire le Table View Programming Guide qui explique tous ces concepts clés en détail.
Ouai c'est surtout que le système de recyclage je n'ai pas trop regardé moi étudié.
C'est surtout le concept : Savoir quand ma cellule est crée que je maitrise plus la chose. Pourtant c'est important
Et le fait que j'ai une Custom Cell : je peux faire à partir du .xib la même chose ?
Et init de ma sous-classe ? correspond au inithWithNbName ?
Ou tu parlais du if(cell == nil) dans la fonction cellForRowAtIndexPath ?
Bon je me plonge en parallèle dans la doc.
@Aligator : comme prévu j'ai lu les 97 pages (soit la totalité) de la doc sur les table view
Voilà j'ai retenu pas mal de choses mais rien qui ne résous mon problème : 1) Je suis aveugle 2) Je suis un mauvais lecteur
Tout d'abord : page 46/98 de la doc
The data source, in its implementation of the tableView:cellForRowAtIndexPath: method, returns a configured cell object that the table view can use to draw a row. For performance reasons, the data source tries to reuse cells as much as possible. It first asks the table view for a specific reusable cell object by sending it a dequeueReusableCellWithIdentifier: message. If no such object exists, the data source creates it, assigning it a reuse identifier. The data source sets the cell's content (in this example, its text) and returns it. “A Closer Look at Table View Cells†(page 55) discusses this data source method and UITableViewCell objects in more detail.
If the dequeueReusableCellWithIdentifier: method asks for a cell that's defined in a storyboard, the method always returns a valid cell. If there is not a recycled cell waiting to be reused, the method creates a new one using the information in the storyboard itself. This eliminates the need to check the return value for nil and create a cell manually.
__________________________________________________________________________________________________________________
Dans un second temps (p. 56) : ils expliquent le fonctionnement du dequeuReusableCellWithIdentifier et son avantage sur la mémoire (dernière phrase)
If a cell object is reusable"the typical case"you assign it a reuse identifier (an arbitrary string) in the storyboard. At runtime, the table view stores cell objects in an internal queue. When the table view asks the data source to configure a cell object for display, the data source can access the queued object by sending a dequeueReusableCellWithIdentifier: message to the table view, passing in a reuse identifier. The data source sets the content of the cell and any special properties before returning it. This reuse of cell objects is a performance enhancement because it eliminates the overhead of cell creation.
Le bout de code associé (p. 58)
__________________________________________________________________________________________________________________
Et pour finir dans le paragraphe des Custom Cell ils expliquent que (p. 74) :
The proper use of table view cells, whether off-the-shelf or custom cell objects, is a major factor in the performance of table views. Ensure that your application does the following three things:
Reuse cells. Object allocation has a performance cost, especially if the allocation has to happen repeatedly over a short period"say, when the user scrolls a table view. If you reuse cells instead of allocating new ones, you greatly enhance table view performance.
Avoid relayout of content. When reusing cells with custom subviews, refrain from laying out those subviews each time the table view requests a cell. Lay out the subviews once, when the cell is created.
__________________________________________________________________________________________________________________
J'ai lu et je pense avoir compris le principe globalement:
Grossomodo : Les cellules sont insérées au fur-et-à -mesure dans une sorte de "buffer" pour être recyclées (quand elle sont hors de vue : en gros non visible sur l'écran car l'utilisateur a scrollé par exemple). Quand l'utilisateur scroll : la tableView va aller chercher la cellule associée (associée avec un indentifiant reuseIdentifier) dans ce "buffer de recyclage". Il ne faut pas trop le surchargé de même. Et allouer une cellule à chaque fois c'est MAL : d'ou le système de recyclage. J'ai oublié les cellules attendent gentiment dans le buffer pour être appelées (un peu comme une salle d'accueil chez le docteur)
Le problème étant que moi j'utilise cela pour charger ma cellule : j'ai l'impression que ça n'a rien à voir avec ce qui a dans la doc et que je ne peut faire ce qu'il font avec le initWithStyle:
Et aussi ma méthode est vide en même temps. Donc à partir de là je n'ai pas trop compris ce que je dois faire dans cette méthode ? redessiner la cellule, colorié mes machins truc ?
Mais je pense que mon problème de recyclage vient de là . Ai-je raison ?
Non, tu as tort. Le problème de recyclage vient du fait que même si tu charges la cellule depuis un nib, il faut quand même que la tableview connaisse son Reuse Identifier.
Il faut définir le Reuse Identifier dans le fichier xib (dans les propriétés de la UITableViewCell). Autrement, la tableview ne sait pas à quelle famille appartient la cellule et ne peut pas utiliser son mécanisme de recyclage.
Petit intermède: cette méthode pour utiliser des cellules personnalisées, décrite dans la doc d'Apple, est peu pratique. En effet, on n'a pas accès aux sous-vues de la cellule. Ce que conseille Apple est de mettre des entiers dans le champ .tag des vues et d'utiliser la méthode -[UIView subviewWithTag:] pour accéder à chaque sous-vue. Mais c'est une méthode très longue.
En fait, depuis iOS 5, le plus rapide pour avoir des ses propres cellules est de créer une sous-classe de UITableViewCell. À l'intérieur du xib, on pourra alors tirer des outlets entre la cellule et ses sous-vues (plus besoin de tag). Il faut ensuite déclarer la cellule en utilisant la méthode -[UITableView registerNib:forCellReuseIdentifier:]. Enfin, il faut utiliser la méthode -[UITableView dequeueReusableCellWithIdentifier:forIndexPath:] pour obtenir une cellule, soit tout neuve, soit recyclée (la méthode renvoie toujours une cellule, pas la peine de comparer à nil).
C'est une méthode d'init, donc ça sert à mettre les variables d'instance dans un état par défaut...
J'ai déjà défini un reuse Identifier dans le .xib. J'ai aussi ma propre sous classe UITableViewCell ce qui évite les tag.
Ensuite il faut la déclarer ou la cellule (avec cette ligne de code) :
et la méthode dequeu : pareillement il faut l'appeler ou du coup ? par logique cette méthode permet de recycler donc je suppose cellForRow ?
Merci d'ailleurs pour ton explication
Voilà mon code (avec le problème qui persiste)
hello,
Rajoute la ligne dans le viewDidLoad par exemple et non dans cellforRow..
Samir veut dire la ligne -registerNib:forReuseIdentifier:. Cette méthode ne doit être appelée qu'une fois.
Et j'ai bien écrit -dequeueReusableCellWithIdentifier:forIndexPath:.
Et cell ne sera jamais == nil.
Bah la pour le coup ça m'a un peut tout chamboulé.
1) j'ai des cellules non initialisé pourtant j'ai reçu leur donnée (vérification via NSLog)
2) j'ai parfois 1 ou 2 doublons
MAJ : @Ceroce le -dequeueReusableCellWithIdentifier:forIndexPath:. est le problème quand je n'utilise pas le forIndexPath tout se passe bien.
Quel différence y a t-il entre avec et sans le paramètre ?
MAJ 2 : En fait ça marche : mais mes boutons sont de-selectionné quand je scroll (hors de la cellule) et que je reviens sur la cellule (sans le forIndexPath)
Voilà le code en question
viewDidLoad
et cellForRowAtIndexPath
____
Mais pour le coup je n'utilise plus (cellForRowAthIndexPath:) :
MAJ : et je ne sais pas si c'est une coincidence mais si je sélectionne ma dernière cellule : à partir d'elle toutes les 4 cellules un bouton est sélectionné aussi : il y a une sorte de propagation toute les 4 cellules ... bref problème de recyclage persistant j'ai l'impression. Ai-je oublié quelques chose ?
MAJ : J'ai rajouté du code pour ceux qui se sentent prêt à comprendre mon problème. Enfin pour ceux qui ont le temps
J'ai enregistré une vidéo : pour vous montrer mon problème. (vidéo sur dropbox : cela dépend de votre connexion mais ça peut galérer à lire la vidéo mais il est préférable de la télécharger puis l'ouvrir sur votre lecteur vidéo)
Comme pas mal de développeur je n'aime pas trop rester bloqué sur un problème comme celui-ci.
Je pense avoir fait le nécessaire : lecture totale de la doc + affichage du code au dessus et adaptation aux remarque de (@Ceroce et @samir) + vidéo :P
Je prépare une corde pour ce soir