[RESOLU][Swift][Dessin d'un NSTextContainer dans une UIViewCollection]
J'ai voulu afficher des textes dans une UIViewCollection. Pas difficile, il suffit de créer une UICollectionViewCell personnalisée contenant un UITextView.
class UneCelluleTexte : UICollectionViewCell {
let textView = UITextView()
override init(frame: CGRect) {
super.init(frame: frame)
textView.frame = CGRectMake(0, 0, frame.size.width, frame.size.height)
textView.selectable = false
textView.editable = false
contentView.addSubview(textView)
}
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
Et de fournir les textes à la demande dans la datasource :
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier(reuseIdentifier, forIndexPath: indexPath) as! UneCelluleTexte
cell.textView.text = mesTextes[indexPath.row]
return cell
}
Ensuite j'ai cherché à remplacer le tableau de texte par un NSTextStorage, un NSLayoutManager et un tableau de NSTextContainer, en me disant qu'il suffisait de passer un NSTextContainer au UITextView contenu dans la cellule.
Sauf que cela n'est pas possible. On ne peut pas modifier le textContainer d'un textView après sa création. Il faut créer un nouveau UITextView en lui indiquant sa frame et le NSTextContainer. Ce n'est pas compatible avec le coté réutilisation d'une cellule générique de UIViewCollection (ou alors je n'ai pas tout compris).
Quelqu'un a-t-il une idée sur la manière dont je pourrais afficher le contenu d'un NSTextContainer dans une cellule d'une UIViewCollection ?
Je l'ai déjà fait dans un UIScrollView, en créant un UITextView à partir d'un NSTextContainer et en le posant la bonne place. Mais là je séche.
Je voudrais obtenir quelque chose dans ce style :
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier(reuseIdentifier, forIndexPath: indexPath) as! UneCelluleTexte
cell.jeSaisPasQuoi = mesTextContainers[indexPath.row]
return cell
}
EDIT : En me plongeant dans la doc j'ai l'impression de ne pas avoir tout compris la première fois. Je devrais pouvoir me débrouiller en utilisant une cellule de type UICollectionReusableView au lieu d'une UIViewCollectionCell. Je verrais ça demain, l'esprit reposé.
Réponses
ça marche .. presque ! Il me reste un problème plutôt curieux.
Commençons par ce qui marche :
J'ai écrit un modèle de UIViewCollectionCell vide servant de support à mon UITextView. Exemple d'utilisation dans la DataSource de la ViewCollection :
La cellule vide détruit le UITextView lors du recyclage, pour retrouver son état initial avant réutilisation.
Cela fonctionne très bien. Les cellules s'affichent les unes après les autres, la charge mémoire reste constante. À ce stade je n'utilise le UITextView que d'une manière ordinaire, en n'affichant qu'une NSAttributedString. Tout vas bien. Je fais un autre post pour l'étape suivante.
Pour la phase suivante je construit le UITextView à partir d'un NSTextContainer.
Et ça marche très bien. Du moins tant que je fabrique le textContainer dans le DataSource. Si je tente d'utiliser un textContainer en provenance de l'extérieur, les problèmes commencent (voir post suivant).
Les choses se gâtent quand je crée le NSTextContainer dans le viewDidLoad du contrôleur.
Création du UITextView dans la DataSource :
Je lance l'exécution :
- la première page est correctement affichée
- la seconde page est correctement affichée
- la page 3 est vide
- les suivantes aussi
- si je revient sur les premières pages elles sont VIDES aussi !
En gros ça marche bien tant que le recyclage n'est pas lancé. Ensuite, plus rien ne fonctionne. J'ai vérifié en mettant des println() dans le prepareForReuse. Si je retire l'effacement du textView, tout fonctionne bien (mais la charge mémoire explose au fur et à mesure de l'utilisation, bien évidement).
Une idée, une suggestion ?
On dirais que le textView.removeFromSuperView() affecte le NSTextContainer utilisé pour construire le textView. J'ai pourtant déjà utilisé une technique similaire en effaçant des UITextView d'une UISCrollView, sans avoir d'ennuis. Mais c'était un effacement pur et dur, pas un recyclage.
J'ai fait des essais avec un textStorage très long (environ 10 pages), découpé en plusieurs textContainer de la taille d'une page. Certaine pages disparaissent lors du déplacement, pour revenir plus tard. J'en conclus que le recyclage de la ViewCollection doit mettre les cellules UITextView en stand-by un certain temps, bloquant l'usage du NSTextContainer correspondant, de manière à faire le nettoyage quand cela ne risque pas d'affecter la fluidité de l'interface.
Conclusion, les NSTextView ne sont pas réutilisables dans une cellule de ViewCollection. Il faut les créer dans le délégué de la collection pour un usage one-shot.
Autre piste : utiliser la méthode drawGlypsForGlyphRange() de NSLayoutManager pour dessiner directement le contenu d'un NSTextContainer sur une vue, sans passer par un UITextView. Je m'y colle quand j'aurais 2 minutes de libre.
Et hop, une view maison pour afficher des NSTextContainer :
Utilisation dans une cellule d'une UICollectionView :
Utilisation dans la DataSource de la collectionView :
Ouf ! ça marche, du moins avec mes premiers tests.
EDIT : Mais pas les seconds tests. Il se passe des choses louches quand j'essaie d'afficher des NSTextContainer appartenant à un même NSLayoutManager sur plusieurs UIViewTextContainer. Une fois encore, cela semble se produire après le premier recyclage de cellule de la viewCollection.
Oups, j'ai perdu l'habitude d'utiliser setNeedsDisplay() en Swift, à tel point que j'ai oublié de l'appeler en changeant la propriété textContainer de mon composant graphique. Voici une version corrigée :
C'était la source de mes problèmes, la vue personnalisée ne se dessinant qu'à la création, pas après un recyclage de cellule.
Exemple d'utilisation :
Au final j'ai obtenu ce que je voulais, un affichage parfaitement fluide, y compris sur iPhone 4, alors qu'il y avait parfois des irrégularités dans le scrolling avec un UIScrollView. C'est ce genre de petit détail qui améliore l'expérience utilisateur.