UITextView, UIGesture & NSMutableAttributedString

Salut !


 


Je me demandais si c'était possible de changer dynamiquement certaines portions d'un Textview comme la font, leur couleur, etc. au toucher.


 


Imaginons le cas suivant : J'ai un TextView avec le texte ci-dessous.


 


"Je suis sur le forum Cocoacafe." 


 


Si je touche le mot "forum", j'aimerais qu'il passe de couleur noir en vert par exemple.


Ou mieux encore, si je sélectionne une partie du texte comme "le forum Cococafe", j'aimerais qu'il me le change en orange.


 


Voilà  mon besoin.


 


Je sais faire tout ça mais sans l'utilisation des Gestures avec NSMutableAttributedString par exemple or j'aimerais le faire avec ces dernières afin que ce soit dynamique. Je ne sais pas si c'est possible d'une part et le moyen de faire quelque chose de similaire d'autre part.


Réponses

  • Le délégué du TextView peut réagir à  la sélection d'une partie du texte. Regarde la classe UITextViewDelegate.


  • Ah ouais !! J'avais même pas fais attention que le délégué et même que le TextView avait une propriété de sélection ! Merci Draken.


  • AliGatorAliGator Membre, Modérateur
    Tu veux avoir le contrôle, genre que sur un tap ça te change la couleur en vert, ou tu veux laisser le contrôle à  l'utilisateur, genre qu'il puisse choisir de mettre le texte sélectionné en gras, italique, etc ? Car sinon il suffit de cocher la case "Editable" du UITextView et c'est fini, tu l'as ton mode édition

    Mais sinon oui UITextView (encore + depuis les dernières versions d'iOS où les APIs se sont grandement enrichies sur TextKit et tout ce qui tourne autour de la manipulation fine du texte) te permet de faire beaucoup de choses, d'autant plus qu'il implémente aussi le protocole UITextInput qui lui-même te rajoute pas mal de méthodes dédiées à  la manipulation du texte et les méthodes de saisie.
  • Je pensais aux 2 cas mais ce serait plutôt de laisser la main à  l'utilisateur mais avec des limites, par exemple juste changer la couleur ou la Font mais pas le texte ou son orthographe.


     


    Je remarque que le mode édition me permet par exemple de changer le texte (ce que je ne veux pas) mais pas forcément de changer sa couleur.


     


    Je pensais également au tap parce que c'est rapide. Si je touche un mot, qu'il se transforme immédiatement en une autre couleur. Implémenter les deux serait une bonne option finalement même si le mode édition suffirait si on peut le limiter.


     


    Faut que je regarde si on peut limiter le mode édition et l'enrichir comme je le souhaiterais mais effectivement je suis surpris de tout ces changements sur le TextView. J'avais pas trop suivis ses modifications au cours de ces dernières années. C'est top !!


  • AliGatorAliGator Membre, Modérateur
    juillet 2015 modifié #6
    Oui, avec les dernières versions d'iOS tu as accès à  pas mal de choses.

    Si tu veux que sur un appui d'un bouton tu puisses changer des attributs du texte sélectionné, c'est facile de récupérer le selectedText et le selectedRange pour savoir ce qui est sélectionné, puis de faire mumuse avec la NS(Mutable)AttributedString pour changer les attributs à  cet endroit.

    Si tu veux dans un 2ème temps pouvoir juste taper sur un mot (sans avoir à  le sélectionner avant) et détecter automatiquement quel mot est tapé, c'est aussi possible :
    • soit en demander à  TextKit (la nouvelle couche de gestion de texte apparue dans les dernières versions d'iOS) de convertir le CGPoint du tap en position dans le texte (enfin de trouver le caractère le plus proche du point tapé, quoi), et ainsi d'en déduire le mot auquel ce caractère appartient et modifier les attributs de la NSAttributedString pour ce mot.
      [EDIT] Même pas besoin de TextKit et ses NSLayoutManager et autres trucs compliqués en fait, t'as déjà  tout dans le protocole UITextInput en fait, voir post suivant[/EDIT]
    • soit en trichant un peu, tu pourrais faire de chaque mot que tu veux rendre "tappable" qu'il soit un lien, avec une URL de ton choix (par exemple "tap://" tout simplement, pas besoin que ça soit une URL vers un site web), et implémenter la méthode de delegate "textView:shouldInteractWithURL:inRange:". Comme ça quand tu tapes sur un mot, qui se trouve être un lien, cette méthode va être appelée, et dedans tu as le NSRange et donc tu sais quels attributs de ta NSAttributedString changer pour changer la couleur.
    Bien sûr avec cette dernière astuce (voir aussi mes petits articles sur Le wiki de mon repo OHAttributedStringAdditions, tous tes mots seront des liens, donc (1) tu peux demander à  ta UITextView d'utiliser un style standard pour tes liens, plutôt qu'il ne les souligne et mette en bleu " mais je me demande du coup si ce réglage ne risque pas d'overrider la couleur du mot, le mot risquant de se mettre de la couleur déclarée "couleur pour les liens" plutôt que de la couleur qu'on lui a donné s'il n'était pas un lien... et sinon (2) cette astuce ne marche évidemment que si tu ne comptais pas NON PLUS mettre des vrais liens cliquables dans ton texte.

    Au final utiliser les méthodes de TextKit pour convertir le CGPoint tapé en index de caractère dans ton texte ce n'est pas si méchant et ça reste plus flexible. Une fois que tu as l'index du caractère, avoir le NSRange du mot autour c'est vite faisable aussi (avec les méthodes de UITextInput tu devrais avoir tout ce qu'il faut), et mettre en couleur bah ça c'est juste NSMutableAttributedString (et là  mon pod OHAttributedStringAdditions pourrait te faciliter la vie)
  • Ok parfait.


     


    J'ai fais ce matin le premier cas. Pour le tap je vais voir TextKit car effectivement ça à  l'air intéressant même si je ne comprends pas très bien comment je vais retrouver le mot si il me retourne qu'un seul et unique caractère ? 


     


    Ce qui est encore plus intéressant c'est que j'ai vu qu'on pouvait ajouter des options dans la popup noir qui s'affiche pour copier, couper etc. Par exemple ajouter des styles comme le gras toutefois j'ai pas vu comment retirer toutes les options par défaut (comme le copier, collé et coupé) si c'est possible.


  • AliGatorAliGator Membre, Modérateur
    juillet 2015 modifié #8

    même si je ne comprends pas très bien comment je vais retrouver le mot si il me retourne qu'un seul et unique caractère ?

    Regarde du côté du UITextInputTokenizer.

    UITextView se conforme au protocole UITextInput, ce qui veut dire qu'il possède une propriété "tokenizer" retournant un UITextInputTokenizer adapté à  ta UITextView et à  qui tu vas pouvoir demander quelle est la position à  gauche de ton curseur qui tombe sur une bordure (boundary) de mot " sauf si t'es déjà  sur le bord d'un mot bien sûr. Tu fais pareil à  droite, et tu auras donc ton début et fin de mot.

    D'ailleurs, en regardant un peu + le protocole UITextView il y a bien + que ce dont je me souvenais, et finalement tu peux faire tout ce que tu cherches à  faire avec.
    • Tu as par exemple une méthode "closestPositionToPoint:" qui te retourne la UITextPosition à  partir d'un CGPoint (typiquement le CGPoint indiquant où tu auras tapé).
    • A partir de cette UITextPosition et en utilisant le "tokenizer" de ta UITextView tu peux regarder à  gauche et à  droite pour trouver le début et fin du mot
    • Et enfin pour finir tu peux convertir tes 2 UITextPosition, représentant le début et la fin du mot, en des index de caractères (pour construire un NSRange), en utilisant la méthode "offsetFromPosition:toPosition:" (en passant beginningOfDocument comme position de référence en premier paramètre)
    • Reste plus qu'à  utiliser ces index pour construire un NSRange et changer l'attribut de ta NSAttributedString pour les caractères dans ce NSRange.
  • Génial ! Elle marche très bien ta solution Ali. Merci.


     


    Je récupère bien une range satisfaisante qui correspond à  l'endroit où je tape. Par contre, étrangement, lorsque je crée un NSMutableAttributedString dans mon cas, il ne me colorie pas seulement le mot tapé mais des lignes entières après celui-ci bien que la range soit bonne. Je ne sais pas pourquoi j'ai ce résultat. Je dois vérifier.

  • AliGatorAliGator Membre, Modérateur
    Tu utilises mon pod OHAttributedStringAdditions pour appliquer ton changement de style sur le NSRange de ta NSMutableAttributedString ? Ou tu fais ça à  la main ?
  • AliGatorAliGator Membre, Modérateur

    Génial ! Elle marche très bien ta solution Ali. Merci.
     
    Je récupère bien une range satisfaisante qui correspond à  l'endroit où je tape. Par contre, étrangement, lorsque je crée un NSMutableAttributedString dans mon cas, il ne me colorie pas seulement le mot tapé mais des lignes entières après celui-ci bien que la range soit bonne. Je ne sais pas pourquoi j'ai ce résultat. Je dois vérifier.

    Tu es sûr que le NSRange est bon? N'oublie pas qu'un NSRange n'est pas défini par une position de début et une position de fin, mais par une position de début... et une longueur !

    Donc quand tu construis ton NSRange, pour le "location" utilises "offsetFromPosition(beginningOfDocument, startTextPosition)", mais pour le "length", n'utilises pas "offsetFromPosition(beginningOfDocument, endTextPosition)" (qui te retournerait la *position* du caractère de fin), mais plutôt "offsetFromPosition(startTextPosition, endTextPosition)" (qui te retournera bien la distance entre le début et la fin).

  •  


     


    Tu utilises mon pod OHAttributedStringAdditions pour appliquer ton changement de style sur le NSRange de ta NSMutableAttributedString ? Ou tu fais ça à  la main ?

     


    À la main. J'avais commencé comme ça parce que ce n'est pas très compliqué même si ton pod semble très complet ;)


     



     


     


    Tu es sûr que le NSRange est bon? N'oublie pas qu'un NSRange n'est pas défini par une position de début et une position de fin, mais par une position de début... et une longueur !

    Donc quand tu construis ton NSRange, pour le "location" utilises "offsetFromPosition(beginningOfDocument, startTextPosition)", mais pour le "length", n'utilises pas "offsetFromPosition(beginningOfDocument, endTextPosition)" (qui te retournerait la *position* du caractère de fin), mais plutôt "offsetFromPosition(startTextPosition, endTextPosition)" (qui te retournera bien la distance entre le début et la fin). 

     


    Oui tu as raison, Je m'en étais rendu compte après. J'avais zappé qu'ici ce n'est pas à  partir du départ et que c'était Location-Length  ::)


    Merci bien pou ton aide, surtout que j'ai lu un peu tout ce qui tourne autour de TextKit grâce à  toi et il y a vraiment des choses intéressantes notamment l'exclusionPaths. Si j'avais su qu'il existait quelque chose comme ça plus tôt j'aurais fais plein de choses différemment.

  • AliGatorAliGator Membre, Modérateur

    À la main. J'avais commencé comme ça parce que ce n'est pas très compliqué même si ton pod semble très complet ;)

    Oui mon pod n'a rien de révolutionnaire en soi, c'est juste un wrapper pour avoir une API sympa. Mais ça a plusieurs avantages, en particulier éviter d'avoir à  faire des allers-retours dans la doc pour retrouver le nom de la constante à  utiliser comme clé pour tel ou tel attribut, et quel type de valeur il faut mettre en face de cet attribut, etc.

    Pour changer la couleur du texte, ça va c'est pas méchant. Mais quand tu commences à  vouloir modifier les attributs de paragraphe (indentation, alignement, lineSpacing, ...) tu peux vite te perdre dans les bons types à  mettre, alors qu'avec mon API comme ce sont des méthodes au nom explicites et avec une signature qui attend explicitement le bon type, tu ne peux pas te tromper.

    Et puis mon pod apporte des choses qui ne sont pas possibles directement en jouant bêtement avec les attributs :

    - Par exemple si tu veux commencer à  mettre ton texte en gras, là  c'est une toute autre histoire. Tu pourrais croire que c'est aussi simple que de changer la couleur, mais il n'y a pas d'attribut pour ça, car en fait pour mettre en gras il faut changer la police en sa variante gras (genre Helvetica --> HelveticaBold). Si tu sais que tu utilises une UIFont particulière en dur, et que tout ton texte de ta NSAttributedString utilise cette font, tu peux toujours coder également en dur sa variante en gras. Mais si tu ne sais pas à  l'avance quelle police est utilisée, pour déduire quelle est le nom de la UIFont qui correspond à  la même police mais en gras, c'est pas si simple.

    - Sans parler du fait que tu peux vouloir mettre en gras une portion de texte qui mélange 2 polices (disons Helvetica et Verdana), et donc qu'il faut passer les morceaux qui sont en Helvetica vers HelveticaBold, et ceux qui sont en Verdana en Verdana-Bold, même si dans la portion que tu veux mettre en gras ça n'arrête pas d'alterner entre les deux...

    Or mon pod gère tous ces cas-là , te permettant juste d'appeler "setTextBold:range:" sans te poser de questions, et il fait le boulot pour toi ;)
  • Et il gère les Font non natives, des fonts spécialement ajoutées au projet si on applique l'exemple avec le texte en gras ?


     


    J'y penserais la prochaine fois. C'est vrai que c'est pas facile de retenir les attributs, même les plus simples.


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