[PROBABLEMENT RESOLU] [Swift] Chargement d'un fichier RTF dans un tableau de NSAttributedString
Draken
Membre
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 ?
Mots clés:
Connectez-vous ou Inscrivez-vous pour répondre.
Réponses
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
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.
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 *
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.
Exemple d'usage :
Merci. Ali, la Doc c'est Lui !
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.
J'ai légèrement modifié le programme pour mesurer le temps d'exécution des méthodes. Le résultat est .. hallucinant.
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 ?
T'as mesuré avec Instruments ?
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.
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 ^^
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...
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.
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 !
116 ms en calculant le nsrange à partir du précédent. C'est nettement mieux.
L'instruction :
est terriblement lente. Y-a-t-il un moyen de faire le calcul plus rapidement ?
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.
Je met la touche finale à ma solution et j'édite ce post !
Voilà :
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 !
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 ..
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.
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.
Il ne reste plus qu'à tester dans différents cas de figures, pour être certain. Merci l'utf16 !
- 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)
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.
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.
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.
[EDIT] Retrouvé : https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/Strings/Articles/stringsClusters.html
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
Un an aprés
Le code inspiré par celui d'AliGator ne compile plus avec Xcode 7.3, qui refuse cette syntaxe :
Il lui faut maintenant :
La version corrigée :
Cela ne compile plus avec Xcode 7.3 ! La seule chose constante dans le monde, c'est le changement ..