[PROBABLEMENT RESOLU] [Swift] Chargement d'un fichier RTF dans un tableau de NSAttributedString

DrakenDraken Membre
juin 2015 modifié dans Objective-C, Swift, C, C++ #1

J'ai écrit un peu de code pour charger le contenu d'un fichier RTF dans un tableau de String.



// CHARGEMENT DU FICHIER
let urlFichier = NSBundle.mainBundle().URLForResource("File", withExtension: "rtf")
var monRTF = NSAttributedString(fileURL: urlFichier,
options: [NSDocumentTypeDocumentAttribute:NSRTFTextDocumentType],
documentAttributes: nil,
error: error)

// CONVERSION NSAttributedString => String
let leTexte = monRTF?.string as String!

// DECOUPAGE DU TEXTE EN LIGNE
let mesLignes = leTexte.componentsSeparatedByCharactersInSet(NSCharacterSet.newlineCharacterSet())

// TRAITEMENT DU TEXTE
for ligne in mesLignes {
println(ligne)
}


J'utilise la méthode componentsSeparateByCharactersInSet() pour découper le texte brut en ligne. ça fonctionne, mais je perds l'enrichissement du texte en convertissant le NSAttributedString en String. 


 


La méthode componentsSeparateByCharactersInSet est bien pratique pour découper mon texte ligne à  ligne, mais elle n'existe pas pour la classe NSAttributedString. Existe-t-il un moyen simple pour faire la même chose en récupérant des NSAttributedString à  la sortie, pour préserver l'enrichissement du texte ?


 


 


 


 


 


«1

Réponses

  • FKDEVFKDEV Membre
    Tu peux refaire la fonction toi-même à  base de

    NSString rangeOfCharacterFromSet:options:range:

    NSAttributedString attributedSubstringFromRange:


    Ou trouver un bout de code déjà  écrit et testé :

    https://github.com/omnigroup/OmniGroup/blob/master/Frameworks/OmniFoundation/OpenStepExtensions.subproj/NSAttributedString-OFExtensions.m
  • CéroceCéroce Membre, Modérateur
    Quel est le but final ?

    Je veux dire, que si c'est pour de l'affichage, tu as peut-être plutôt intérêt d'utiliser Text Kit, voire une Webview.
  • DrakenDraken Membre
    mars 2015 modifié #4


    Tu peux refaire la fonction toi-même à  base de

    NSString rangeOfCharacterFromSet:options:range:

    NSAttributedString attributedSubstringFromRange




    C'est ce que j'ai l'intention de faire, si je ne trouve pas de solution simple.


     





    De l'Objective-C de l'époque pré-ARC ! * grimace *


    Bon, je comprend pourquoi je n'ai pas trouvé en faisant des recherches, je me cantonnais à  des sources Swift. * mode feignant et fier de l'être *


     




    Quel est le but final ?

    Je veux dire, que si c'est pour de l'affichage, tu as peut-être plutôt intérêt d'utiliser Text Kit, voire une Webview.




    L'idée est de créer une mini-application de PAO, permettant à  un utilisateur de charger un texte dans un format libre, pour le mettre en page semi-manuellement, selon un format précis. Au final c'est bien TextKit qui se charge de l'affichage.

  • AliGatorAliGator Membre, Modérateur
    extension NSAttributedString {
    func enumerateSubstrings(_ options: NSStringEnumerationOptions = .ByLines, body: (NSAttributedString)->() ) {
    let fullRange = self.string.startIndex..<self.string.endIndex
    self.string.enumerateSubstringsInRange(fullRange, options: options) { (_, range, _, _) -> () in
    let nsrange = NSMakeRange(distance(self.string.startIndex, range.startIndex), count(range))
    let substr = self.attributedSubstringFromRange(nsrange)
    body(substr)
    }
    }
    }

    Exemple d'usage :

    attrStr.enumerateSubstrings(.ByLines) { attributedLine in
    println("line string = \(attributedLine.string)")
    }
  • DrakenDraken Membre
    mars 2015 modifié #6

    Merci. Ali, la Doc c'est Lui !


  • DrakenDraken Membre
    juin 2015 modifié #7

    Je reviens sur le sujet, ayant effectué des tests sur device, suite à  l'achat d'une licence développeur.  Le découpage d'un tableau de NSAttributedString est extrêmement lent sur mon iPhone4 (iOS 7.1). Cela prends plusieurs secondes ( ???) pour découper un texte de 100 lignes, alors que c'est quasi-instantané avec des Strings.


     


    J'ai écrit un petit programme de test pour mettre le phénomène en évidence.



    import UIKit

    extension NSAttributedString {
    func enumerateSubstrings(_ options: NSStringEnumerationOptions = .ByLines, body: (NSAttributedString)->() ) {
    let fullRange = self.string.startIndex..<self.string.endIndex
    self.string.enumerateSubstringsInRange(fullRange, options: options) { (_, range, _, _) -> () in
    let nsrange = NSMakeRange(distance(self.string.startIndex, range.startIndex), count(range))
    let substr = self.attributedSubstringFromRange(nsrange)
    body(substr)
    }
    }
    }

    func decoupageTexte (texte:NSAttributedString) -> [NSAttributedString] {
    var tableau = [NSAttributedString]()
    texte.enumerateSubstrings(.ByLines) { attributedLine in
    tableau.append(attributedLine)
    }
    return tableau
    }

    class ViewController: UIViewController {

    override func viewDidLoad() {
    super.viewDidLoad()

    let monTexte = self.creerTexte()

    // String classique
    println("début découpage string")
    let tableauStrings = monTexte.string.componentsSeparatedByCharactersInSet(NSCharacterSet.newlineCharacterSet())
    println("fin découpage string")

    // NSAttributedString
    println("début découpage NSAttributedString")
    let tableauAttr = decoupageTexte(monTexte)
    println("fin découpage NSAttributedString")

    }

    func creerTexte() -> NSAttributedString {

    let t1 = NSAttributedString(string: "Il est difficile de dater avec certitude l'apparition des premiers sushis. Elle aurait eu lieu aux alentours du ve siècle av. J.-C., date à  laquelle la riziculture s'installa au Japon.\n")

    var texte = NSMutableAttributedString(attributedString: t1)

    for _ in 1...100 {
    texte.appendAttributedString(t1)
    }

    return texte
    }

    }


  • J'ai légèrement modifié le programme pour mesurer le temps d'exécution des méthodes. Le résultat est .. hallucinant. 



    override func viewDidLoad() {
    super.viewDidLoad()

    let monTexte = self.creerTexte()

    // String classique
    let t1 = NSDate.timeIntervalSinceReferenceDate() * 1000
    let tableauStrings = monTexte.string.componentsSeparatedByCharactersInSet(NSCharacterSet.newlineCharacterSet())
    let t2 = NSDate.timeIntervalSinceReferenceDate() * 1000
    println("Strings : \(t2-t1) ms")

    // NSAttributedString
    let t3 = NSDate.timeIntervalSinceReferenceDate() * 1000
    let tableauAttr = decoupageTexte(monTexte)
    let t4 = NSDate.timeIntervalSinceReferenceDate() * 1000
    println("NSAttributedString : \(t4-t3) ms")

    }


    L'échantillon de base est une NSAttributedString contenant 100 fois la chaà®ne:


     


     "Il est difficile de dater avec certitude l'apparition des premiers sushis. Elle aurait eu lieu aux alentours du ve siècle av. J.-C., date à  laquelle la riziculture s'installa au Japon.\n"


     


    Sur le simulateur (résultats quasi-identiques en iOS7.1 et iOS8.3) :


    Extraction des 100 substrings : 1,7 ms


    Extraction des 100 NSAtttributedString : 562 ms


     

    Sur l'iPhone 4 (iOS 7.1) :


    Extraction des 100 substrings : 17 ms


    Extraction des 100 NSAtttributedString : 4900 ms


     


    Je me suis trompé dans mon protocole de mesure ? J'ai fait une grosse erreur quelque part ? 

  • CéroceCéroce Membre, Modérateur

    T'as mesuré avec Instruments ?


  • DrakenDraken Membre
    juin 2015 modifié #10

    Non, j'ai essayé, mais je n'arrive pas à  l'utiliser pour mesurer le temps d'exécution d'une méthode. C'est pourquoi j'ai ajouté du code pour mesurer le temps dans le corps du programme, à  l'ancienne. Mais de toute façon, la différence de temps est bien perceptible à  "l'echelle humaine", surtout quand une vue met plusieurs secondes à  s'afficher.

  • PyrohPyroh Membre
    juin 2015 modifié #11

    Ton protocol de test n'est pas idéal mais fait correctement le boulot que tu lui demande.


    Disons que tu n'as pas le temps exact mais les 2 relevés ayant la même marge d'erreur tu mets bien en évidence ce que tu cherche.


     


    Je vais faire 2/3 tests de mon côté. Tu compile en Swift 1.2 ou 2.0 ? Le code m'a déjà  répondu tout seul ^^


  • AliGatorAliGator Membre, Modérateur
    La manipulation des NSAttributedString sera toujours plus complexe que manipuler du texte simple, car c'est une représentation plus complexe, et demander à  iOS de couper une NSAttributedString en 2 nécessite qu'il vérifie si tu ne coupes pas en plein milieu d'un attribut (par exemple en plein milieu d'une chaà®ne qui a été mise en gras et rouge), pour réappliquer ces attributs correctement après la découpe.

    Mais c'est vrai que là  la différence est quand même importante.
    Je ne saurai que te conseiller comme Céroce de tester avec Instruments (Time Profiler) pour voir *vraiment* quelle ligne est la plus gourmande et quelle est la partie de ton algo qui bouffe tant de temps

    Après, comme ça au feeling, à  mon avis le plus consommateur c'est la méthode "enumerateSubstrings", surtout du fait que :
    1) tu redemandes la self.string à  chaque fois
    2) tu dois créer un Swift.Range à  partir d'un NSRange et donc utiliser "distance" (qui fait une itération caractère par caractère si je ne m'abuse, pour être sûr que ça compte en nombre de caractères ou glyphes et pas en nombre de codepoints Unicode).
    3) Puis tu découpes la NSAttributedString, ce qui comme décrit plus haut doit sans doute demander à  iOS un peu de traitement + consommateur que pour découper une simple NSString

    La première chose à  faire c'est de créer "let str = self.string" et d'utiliser cette constante plutôt que self.string dans le reste du code. Même si honnêtement je ne pense pas que ça soit l'action la plus consommatrice, mais bon.

    La 2ème c'est peut-être réfléchir à  voir comment optimiser la création du Swift.Range, peut-être en évitant de calculer la distance à  chaque fois depuis le début de str.startIndex, mais en repartant par exemple du endIndex du dernier Range, pour avoir moins d'incrémentation d'index à  faire calculer à  la méthode "distance" de Swift.

    La 3ème chose c'est de te poser la question de savoir est-ce que c'est logique / judicieux / nécessaire vu ton besoin de vraiment avoir les NSAttributedString ou est-ce qu'avoir juste un tableau des Ranges ne te suffirait pas, pour ne faire le vrai découpage que plus tard ? (En fait ça dépend pas mal de ton besoin)

    La 4ème chose c'est de repasser Instruments, car de toute façon ça sert à  rien d'optimiser à  l'aveugle et de risquer de passer du temps à  optimiser du code si ce n'est pas ce code là  qui est lent mais un truc qui n'a en fait rien à  voir...
  • DrakenDraken Membre
    juin 2015 modifié #13

    J'ai fait des tests plus poussés. C'est le calcul du nsrange qui fait exploser le compteur. Si je le remplace par une valeur fixe bidon de 200 caractères, le temps de découpage passe de 4740 ms à  .. 21 ms. 



    extension NSAttributedString {
    func enumerateSubstrings(_ options: NSStringEnumerationOptions = .ByLines, body: (NSAttributedString)->() ) {
    let str = self.string
    let fullRange = str.startIndex..<self.string.endIndex
    str.enumerateSubstringsInRange(fullRange, options: options) { (_, range, _, _) -> () in
    // let nsrange = NSMakeRange(distance(str.startIndex, range.startIndex), count(range))
    // Range bidon pour test
    let nsrange = NSMakeRange(0, 200)
    let substr = self.attributedSubstringFromRange(nsrange)
    body(substr)
    }
    }
    }


    L'extraction de la subString n'est pas chronophage. En la remplaçant par une NSAttributedString en dur, on ne gagne que 6 ms sur l'ensemble de l'opération (15 ms au lieu de 21 ms). C'est insignifiant !



    extension NSAttributedString {
    func enumerateSubstrings(_ options: NSStringEnumerationOptions = .ByLines, body: (NSAttributedString)->() ) {
    let str = self.string
    let fullRange = str.startIndex..<self.string.endIndex
    str.enumerateSubstringsInRange(fullRange, options: options) { (_, range, _, _) -> () in
    // let nsrange = NSMakeRange(distance(str.startIndex, range.startIndex), count(range))
    // let substr = self.attributedSubstringFromRange(nsrange)
    let substr = NSAttributedString(string: "Il est difficile de dater avec certitude l'apparition des premiers sushis. Elle aurait eu lieu aux alentours du ve siècle av. J.-C., date à  laquelle la riziculture s'installa au Japon.")
    body(substr)
    }
    }
    }


  • DrakenDraken Membre
    juin 2015 modifié #14

    116 ms en calculant le nsrange à  partir du précédent. C'est nettement mieux. 



    extension NSAttributedString {
    func enumerateSubstrings(_ options: NSStringEnumerationOptions = .ByLines, body: (NSAttributedString)->() ) {
    let str = self.string
    let fullRange = str.startIndex..<self.string.endIndex
    var position:Int = 0

    str.enumerateSubstringsInRange(fullRange, options: options) { (_, range, _, _) -> () in
    // let nsrange = NSMakeRange(distance(str.startIndex, range.startIndex), count(range))
    let size = count(range)
    let nsrange = NSMakeRange(position, size)
    position += size + 1
    let substr = self.attributedSubstringFromRange(nsrange)
    body(substr)
    }
    }
    }


    L'instruction :



    count(range)

    est terriblement lente. Y-a-t-il un moyen de faire le calcul plus rapidement ?



    // let size = count(range)
    let size = 184

    Dans mon programme de test, toutes les chaà®nes ont une longueur de 184 caractères. Si je remplace count(range) par 184, l'extraction de l'ensemble des textes se fait en 26 ms, à  la place de 116 ms ! C'est loin d'être négligeable.


  • PyrohPyroh Membre
    juin 2015 modifié #15

    Je met la touche finale à  ma solution et j'édite ce post !


     


    Voilà  :



    public extension NSAttributedString {
    func componentsSeparatedByCharactersInSet(set: NSCharacterSet) -> [NSAttributedString] {
    var array: [NSAttributedString] = []
    let utf16 = self.string.utf16
    let stringStart = utf16.startIndex
    let stringEnd = utf16.endIndex
    var currentIndex = stringStart
    var nextSubstringStartIndex = stringStart

    while currentIndex < stringEnd {
    if set.characterIsMember(utf16[currentIndex]) {
    array.append(self.attributedSubstringFromRange(NSMakeRange(nextSubstringStartIndex - stringStart, currentIndex - nextSubstringStartIndex)))
    nextSubstringStartIndex = currentIndex.successor()
    }
    currentIndex = currentIndex.successor()
    }
    if nextSubstringStartIndex < currentIndex {
    array.append(self.attributedSubstringFromRange(NSMakeRange(nextSubstringStartIndex - stringStart, currentIndex - nextSubstringStartIndex)))
    } else if set.characterIsMember(utf16[currentIndex.predecessor()]) {
    array.append(self.attributedSubstringFromRange(NSMakeRange(stringEnd - stringStart, 0)))
    }

    return array
    }
    }

    Cette fonction renvoie exactement le même résultat pour un NSAttributedString que ce que fait componentsSeparatedByCharactersInSet pour les String.


     


    J'utilise uft16 parce que les NSAttributedString sont en UTF-16 et que les indices correspondent sans besoin de convertir.


     


    Ce code est Swift 2.0 (bien que je pense qu'il passe directement sur 1.2)


     


  • Super !

  • AliGatorAliGator Membre, Modérateur
    J'ai posé la question à  d'autres devs iOS sur Slack et Nate Cook m'a pointé sur sa réponse sur SO : http://stackoverflow.com/questions/25882503/how-can-i-use-nsregularexpression-on-swift-strings-with-variable-width-unicode-c

    C'est vrai que le fait que advance / distance / count soient longs, c'est parce qu'il faut itérer sur chaque caractère de la chaà®ne l'un après l'autre (il n'y a pas de "random access" pour accéder directement au caractère n° X, il faut les parcourir un par un, vu comment UTF8 et Unicode fonctionnent). Alors que si au lieu de manipuler des caractères tu manipules des CodePoints utf16, c'est plus efficace, car chaque CodePoint a alors une largeur fixe (16 bits), donc c'est beaucoup plus efficace à  parcourir (et on peut faire du RandomAccess et accéder directement au CodePoint n°X ou connaà®tre la position de tel CodePoint dans toute la chaà®ne, etc)
  • Oui Ali c'est ce que j'ai fait 3 posts plus haut ^^


     


    Si on utilise uniquement des String on peut tranquillement y aller en UTF-32 (String.UnicodeScalarView). C'est avec NSAttributedString qu'on doit encore travailler en UTF-16 pour être le plus performant.


    J'espère qu'Apple va changer ça ! 


  • Merci à  vous deux !


    Pyroh, ton code compile parfaitement en Swift 1.2, et ne met que 40 ms pour exécuter mon test. On est bien loin des 4900 ms d'hier ..



  • La 3ème chose c'est de te poser la question de savoir est-ce que c'est logique / judicieux / nécessaire vu ton besoin de vraiment avoir les NSAttributedString ou est-ce qu'avoir juste un tableau des Ranges ne te suffirait pas, pour ne faire le vrai découpage que plus tard ? (En fait ça dépend pas mal de ton besoin)




    Effectivement, un tableau des Ranges serait suffisant. J'utilise un UITableView pour afficher les textes, le découpage peut être réalisé à  la volée. 

  • DrakenDraken Membre
    juin 2015 modifié #21

    Record battu : 22 ms (iPhone 4 sous iOS 7.1), en suivant les conseils d'optimisation d'Ali :


    C'est juste 220x plus rapide qu'hier soir ! Et seulement 10% plus lent qu'avec un tableau de String classique.



    extension NSAttributedString {
    func enumerateSubstrings(_ options: NSStringEnumerationOptions = .ByLines, body: (NSAttributedString)->() ) {
    let str = self.string
    let fullRange = str.startIndex..<str.endIndex
    var position:Int = 0

    str.enumerateSubstringsInRange(fullRange, options: options) { (text, _, _, _) -> () in
    let size = count(text.utf16)
    let nsrange = NSMakeRange(position, size)
    position += size + 1
    let substr = self.attributedSubstringFromRange(nsrange)
    body(substr)
    }
    }
    }


    Il ne reste plus qu'à  tester dans différents cas de figures, pour être certain. Merci l'utf16 !

  • AliGatorAliGator Membre, Modérateur
    juin 2015 modifié #22
    Du coup puisque tu utilises l'utf16, lors de tes tests si tu veux stresser un peu et tirer au limites pour valider que ça marche, pense à  utiliser des chaà®nes avec des caractères dont le CodePoint a besoin de 2 words en UTF-16 pour être codés.
    • La majorité des caractères qu'on utilise ont un CodePoint entre U+0000 et U+D7FF ou entre U+E000 et U+FFFF (ce qu'on appelle le "Basic Multilangual Plane", contenant la plupart des caractères communs). En UTF16 ces Caractères/CodePoints sont codé sur un seul mot UTF16 (16 bits, donc), qui vaut directement la valeur du CodePoint
    • Par contre, les CodePoints en dehors de cet intervalle, c-à -d ceux dont le CodePoint est entre U+10000 to U+10FFFF (qu'on appelle les "Supplementary Planes"), nécessitent 2 mots UTF16 (32 bits, ou plus exactement 2 mots de 16 bits qui vont par paire). En effet, ils ces CodePoints (qui tiennent sur 20 bits après leur avoir soustrait 0x10000) sont coupés en 2 " leur 10 premiers bits codés dans le premier mot de 16 bits, les 10 derniers bits dans le 2ème mot de 16 bits.
    Du coup, quand tu as des caractères issus d'un "Supplementary Plane" dans ta chaà®ne, ce caractère va nécessiter 2 mots UTF16 pour être encodé. donc "count(s.utf16)" va retourner 2 (et non 1) si s est une chaà®ne contenant ce caractère. C'est par exemple le cas des Emojis (dont les CodePoints sont ~ entre U+1F300 et U+1F5FF), qui pourraient donc te fausser tes calculs et ton algo.


    (source: Wikipedia: UTF-16)
  • FKDEVFKDEV Membre
    juin 2015 modifié #23

    A mon avis, ce code n'est pas formidable car il repose sur des supositions implicites difficiles à  vérifier quand on n'est pas un expert unicode et NSString.


    D'après ce que je comprends, cela va fonctionner car la représentation interne des NSString est UTF16.


    Par conséquent le count du NSString va être équivalent au count du String.UTF16View.


     


    Après, comme signalé par Ali, il y a le problème des caractères qui sont codés sur plusieurs codes UTF16.


    C'est forcément le cas des caractères le code dépasse 0xFFFF (emoji par exemple).


    Mais il y aussi, le problème des caractères avec diacritiques composés/décomposés, c'est-à -dire quand l'accent sur un caractère est codé sur un caractère à  part ou non.


     


    Par exemple :


    é peut-être encodé sur 16 bits: 0x00e9 dans sa forme précomposée


    ou sur 2 fois 16 bits dans sa forme decomposé : 0x0065 + 0x0301


    (source)


     


    Il faut être sûr que les deux représentations (celle du NSString et celle du StringUTF16View) sont équivalentes à  tous points de vue.


     


     


    Conclusion, je pense qu'il vaudrait mieux faire toutes les manipulations de NSAttributedString en Objective-C en attendant que Les attributed stirng soit entièrement porté en Swift.


     


    Lire les tableaux à  la fin de ce document https://developer.apple.com/library/prerelease/mac/documentation/Swift/Conceptual/Swift_Programming_Language/StringsAndCharacters.html pour comprendre ce que renvoient les méthodes utf16 et UnicodeScaler.


  • DrakenDraken Membre
    juin 2015 modifié #24

    Bon, je vais utiliser la version de Pyroh alors. Autant ne pas courir de risques inutilement. 


     


    C'est bien dommage que les p'tits gars de Cupertino n'aient pas pensé à  implémenter une fonction enumerateSubStrings pour les NSAttributedString.

  • Ben c'est pareil, il faudrait savoir si l'incrementation d'index faite par la méthode successor() est la même qu'une incrémentation dans un NSRange relatif à  un NSString.


     


    Le mieux c'est de calculer ton range sur un NString obtenu à  partir de ton NSMutableString inital.



  • Ben c'est pareil, il faudrait savoir si l'incrementation d'index faite par la méthode successor() est la même qu'une incrémentation dans un NSRange relatif à  un NSString.


     


    Le mieux c'est de calculer ton range sur un NString obtenu à  partir de ton NSMutableString inital.




    J'ai fait mes tests avec des caractères Unicode et des caractères composés.


    Aussi loin que je suis allé (et je suis allé pas mal loin dans les tests et comparatifs) je reste persuadé que NSString == String.UTF16View.


     


    Je trouve une manière élégante de te le prouver et je te la poste. 

  • AliGatorAliGator Membre, Modérateur
    juin 2015 modifié #27
    Oui NSString a toujours stocké en interne les caractères de la chaà®ne au format UTF16. C'est écrit qqpart dans la doc il me semble.

    [EDIT] Retrouvé : https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/Strings/Articles/stringsClusters.html

    NSString objects are conceptually UTF-16 with platform endianness. [...] what it means is that NSString lengths, character indexes, and ranges are expressed in terms of UTF-16 units, and that the term “character” in NSString method names refers to 16-bit platform-endian UTF-16 units.



  • Je trouve une manière élégante de te le prouver et je te la poste. 




     


    T'embête pas, je pense que cela fonctionne. C'est plus une question de principe. Je trouve que c'est plus sûr de rester en Objective-C dans ce genre de cas.


     


     


    Ali,


    Il y a le stockage en UTF16 qui est identique des deux côtés UTF16View et NSString (donc ça colle pour les emoji).


    Mais il y a aussi l'histoire des decomposed/precomposed, là  on ne sait pas trop si lors de la transition


    NSString -> Swift String -> UTF16View


    Tout a bien été conservé tel quel.


    Je suppose que oui car je pense que c'est un choix de l'utilisateur de l'API d'être en decomposed/precomposed.

  • Voilà  j'ai mis un petite playground qui met en évidence la similarité NSString - String.UTF16View


     


  • DrakenDraken Membre
    avril 2016 modifié #30


    116 ms en calculant le nsrange à  partir du précédent. C'est nettement mieux. 



    extension NSAttributedString {
    func enumerateSubstrings(_ options: NSStringEnumerationOptions = .ByLines, body: (NSAttributedString)->() ) {
    let str = self.string
    let fullRange = str.startIndex..<self.string.endIndex
    var position:Int = 0

    str.enumerateSubstringsInRange(fullRange, options: options) { (_, range, _, _) -> () in
    // let nsrange = NSMakeRange(distance(str.startIndex, range.startIndex), count(range))
    let size = count(range)
    let nsrange = NSMakeRange(position, size)
    position += size + 1
    let substr = self.attributedSubstringFromRange(nsrange)
    body(substr)
    }
    }
    }


    L'instruction :



    count(range)

    est terriblement lente. Y-a-t-il un moyen de faire le calcul plus rapidement ?



    // let size = count(range)
    let size = 184

    Dans mon programme de test, toutes les chaà®nes ont une longueur de 184 caractères. Si je remplace count(range) par 184, l'extraction de l'ensemble des textes se fait en 26 ms, à  la place de 116 ms ! C'est loin d'être négligeable.




     


     


    Un an aprés


    Le code inspiré par celui d'AliGator ne compile plus avec Xcode 7.3, qui refuse cette syntaxe :



    let size = count(range) 

    Il lui faut maintenant :



    let size = range.count

    La version corrigée :



    extension NSAttributedString {
    func enumerateSubstrings(options: NSStringEnumerationOptions = .ByLines, body: (NSAttributedString)->() ) {
    let str = self.string
    let fullRange = str.startIndex..<self.string.endIndex
    var position:Int = 0

    str.enumerateSubstringsInRange(fullRange, options: options) { (_, range, _, _) -> () in
    let size = range.count
    let nsrange = NSMakeRange(position, size)
    position += size + 1
    let substr = self.attributedSubstringFromRange(nsrange)
    body(substr)
    }
    }
    }




  • Je met la touche finale à  ma solution et j'édite ce post !


     


    Voilà  :



    public extension NSAttributedString {
    func componentsSeparatedByCharactersInSet(set: NSCharacterSet) -> [NSAttributedString] {
    var array: [NSAttributedString] = []
    let utf16 = self.string.utf16
    let stringStart = utf16.startIndex
    let stringEnd = utf16.endIndex
    var currentIndex = stringStart
    var nextSubstringStartIndex = stringStart

    while currentIndex < stringEnd {
    if set.characterIsMember(utf16[currentIndex]) {
    array.append(self.attributedSubstringFromRange(NSMakeRange(nextSubstringStartIndex - stringStart, currentIndex - nextSubstringStartIndex)))
    nextSubstringStartIndex = currentIndex.successor()
    }
    currentIndex = currentIndex.successor()
    }
    if nextSubstringStartIndex < currentIndex {
    array.append(self.attributedSubstringFromRange(NSMakeRange(nextSubstringStartIndex - stringStart, currentIndex - nextSubstringStartIndex)))
    } else if set.characterIsMember(utf16[currentIndex.predecessor()]) {
    array.append(self.attributedSubstringFromRange(NSMakeRange(stringEnd - stringStart, 0)))
    }

    return array
    }
    }

    Cette fonction renvoie exactement le même résultat pour un NSAttributedString que ce que fait componentsSeparatedByCharactersInSet pour les String.


     


    J'utilise uft16 parce que les NSAttributedString sont en UTF-16 et que les indices correspondent sans besoin de convertir.


     


    Ce code est Swift 2.0 (bien que je pense qu'il passe directement sur 1.2)




     


    Cela ne compile plus avec Xcode 7.3 ! La seule chose constante dans le monde, c'est le changement ..

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