[RESOLU][Swift][Dessin d'un NSTextContainer dans une UIViewCollection]

DrakenDraken Membre
août 2015 modifié dans Objective-C, Swift, C, C++ #1

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 :



    func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {

    let cell = collectionView.dequeueReusableCellWithReuseIdentifier(reuseIdentifier, forIndexPath: indexPath) as! UneCelluleVide

    // Lecture taille cellule
    let size = cell.bounds.size
    let frame = CGRectMake(0, 0, size.width, size.height)

    // Création d'un texte de test (ArialMT corps 18)
    var monTexte = NSAttributedString(string: texte + "\n\n\n" + "PAGE \(indexPath.row)")
    monTexte = changementFont(monTexte, font!)

    // Création TextView
    let textView = UITextView(frame: frame)
    textView.attributedText = monTexte
    textView.editable = false
    textView.selectable = false
    textView.scrollEnabled = false

    // On place le textView sur la cellule
    cell.contentView.addSubview(textView)

    return cell
    }


    La cellule vide détruit le UITextView lors du recyclage, pour retrouver son état initial avant réutilisation.



    class UneCelluleVide : UICollectionViewCell {

    override init(frame: CGRect) {
    super.init(frame: frame)
    }

    override func prepareForReuse() {
    super.prepareForReuse()
    if self.contentView.subviews.count==1 {
    var textView = self.contentView.subviews[0] as! UITextView
    textView.removeFromSuperview()
    }
    }

    required init(coder aDecoder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
    }
    }


    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.

  • DrakenDraken Membre
    août 2015 modifié #3

    Pour la phase suivante je construit le UITextView à  partir d'un NSTextContainer. 



    func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {

    let cell = collectionView.dequeueReusableCellWithReuseIdentifier(reuseIdentifier, forIndexPath: indexPath) as! UneCelluleVide

    // Lecture taille cellule
    let sizeCell = cell.bounds.size
    let frame = CGRectMake(0, 0, sizeCell.width, sizeCell.height)

    // Création du texte
    var monTexte = NSAttributedString(string: texte + "\n\n\n" + "PAGE \(indexPath.row)")
    monTexte = changementFont(monTexte, font!)

    // Création d'un NSTextContainer
    let layoutManager = NSLayoutManager()
    let textStorage = NSTextStorage(attributedString: monTexte)
    textStorage.addLayoutManager(layoutManager)
    let textContainer = NSTextContainer(size: sizeCell)
    layoutManager.addTextContainer(textContainer)

    // Création TextView avec le textContainer
    let textView = UITextView(frame: frame, textContainer: textContainer)
    textView.editable = false
    textView.selectable = false
    textView.scrollEnabled = false

    // On place le textView sur la cellule
    cell.contentView.addSubview(textView)

    return cell
    }


    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).

  • DrakenDraken Membre
    août 2015 modifié #4

    Les choses se gâtent quand je crée le NSTextContainer dans le viewDidLoad du contrôleur.



    class ViewController: UIViewController, UICollectionViewDelegateFlowLayout, UICollectionViewDataSource {

    var collectionView : UICollectionView?
    let font = UIFont(name: "ArialMT", size: 18.0)
    var textStorageTest = NSTextStorage()
    var layoutManagerTest = NSLayoutManager()
    var containerTest = NSTextContainer()

    override func viewDidLoad() {
    super.viewDidLoad()

    self.view = UIView(frame: UIScreen.mainScreen().bounds)

    let layout: UICollectionViewFlowLayout = UICollectionViewFlowLayout()
    layout.sectionInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
    layout.scrollDirection = UICollectionViewScrollDirection.Horizontal
    layout.minimumLineSpacing = 0.0

    collectionView = UICollectionView(frame: self.view.frame, collectionViewLayout: layout)

    collectionView?.dataSource = self
    collectionView?.delegate = self

    self.collectionView?.registerClass(UneCelluleVide.self, forCellWithReuseIdentifier: reuseIdentifier)

    collectionView?.backgroundColor = UIColor.whiteColor()
    collectionView?.pagingEnabled = true
    self.view.addSubview(collectionView!)

    // Création du texte
    var monTexte = NSAttributedString(string: texte + "\n\n\n" + "UNE PAGE")
    monTexte = changementFont(monTexte, font!)

    // Création NSTextContainer
    layoutManagerTest = NSLayoutManager()
    layoutManagerTest.allowsNonContiguousLayout = true
    textStorageTest = NSTextStorage(attributedString: monTexte)
    textStorageTest.addLayoutManager(layoutManagerTest)
    // TextContainer de la taille de l'écran (identique à  celle des cellules)
    containerTest = NSTextContainer(size: UIScreen.mainScreen().bounds.size)
    layoutManagerTest.addTextContainer(containerTest)

    }


    Création du UITextView dans la DataSource :



    func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {

    let cell = collectionView.dequeueReusableCellWithReuseIdentifier(reuseIdentifier, forIndexPath: indexPath) as! UneCelluleVide

    // Lecture taille cellule
    let sizeCell = cell.bounds.size
    let frame = CGRectMake(0, 0, sizeCell.width, sizeCell.height)

    // Création TextView avec le textContainer
    let textView = UITextView(frame: frame, textContainer: self.containerTest)

    textView.editable = false
    textView.selectable = false
    textView.scrollEnabled = false

    // On place le textView sur la cellule
    cell.contentView.addSubview(textView)

    return cell
    }


    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).



    class UneCelluleVide : UICollectionViewCell {

    override init(frame: CGRect) {
    super.init(frame: frame)
    }

    override func prepareForReuse() {
    super.prepareForReuse()
    if self.contentView.subviews.count==1 {
    var textView = self.contentView.subviews[0] as! UITextView
    // textView.removeFromSuperview()
    }
    }

    required init(coder aDecoder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
    }
    }


    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.

  • DrakenDraken Membre
    août 2015 modifié #5

    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.


  • DrakenDraken Membre
    août 2015 modifié #7

    Et hop, une view maison pour afficher des NSTextContainer :



    class UIViewTextContainer : UIView {
    var textContainer:NSTextContainer?

    override init(frame: CGRect) {
    super.init(frame: frame)
    self.backgroundColor = UIColor.ClearColor()
    }

    override func drawRect(rect: CGRect) {
    if let textContainer = textContainer {
    let layoutManager = textContainer.layoutManager
    let range = layoutManager?.glyphRangeForTextContainer(textContainer)
    layoutManager?.drawBackgroundForGlyphRange(range!, atPoint: CGPointMake(0, 0))
    layoutManager?.drawGlyphsForGlyphRange(range!, atPoint: CGPointMake(0, 0))
    }
    }

    required init(coder aDecoder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
    }

    }
     

    Utilisation dans une cellule d'une UICollectionView :



    class UneCelluleTextContainer : UICollectionViewCell {
    let viewContainer = UIViewTextContainer()

    override init(frame: CGRect) {
    super.init(frame: frame)

    viewContainer.frame = CGRectMake(0, 0, frame.size.width, frame.size.height)
    contentView.addSubview(viewContainer)
    }

    required init(coder aDecoder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
    }
    }
     

    Utilisation dans la DataSource de la collectionView :



    func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {

    cell.viewContainer.textContainer = containerTest
    return cell
    }
     

    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.

  • DrakenDraken Membre
    août 2015 modifié #8

    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 :



    class UIViewTextContainer : UIView {

    private var _textContainer:NSTextContainer?

    var textContainer:NSTextContainer? {
    get { return _textContainer }
    set {
    _textContainer = newValue
    self.setNeedsDisplay()
    }
    }

    override init(frame: CGRect) {
    super.init(frame: frame)
    self.backgroundColor = UIColor.whiteColor()
    }

    override func drawRect(rect: CGRect) {
    if let textContainer = self._textContainer {
    let layoutManager = textContainer.layoutManager
    let range = layoutManager?.glyphRangeForTextContainer(textContainer)
    layoutManager?.drawBackgroundForGlyphRange(range!, atPoint: CGPointMake(0, 0))
    layoutManager?.drawGlyphsForGlyphRange(range!, atPoint: CGPointMake(0, 0))
    }
    }

    required init(coder aDecoder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
    }

    }


    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 :



    func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {

    let cell = collectionView.dequeueReusableCellWithReuseIdentifier(reuseIdentifier, forIndexPath: indexPath) as! UneCelluleTextContainer

    cell.viewContainer.textContainer = listeContainers[indexPath.row]
    return cell
    }



    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.


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