Tester les limites Unicode du surlignage iOS sur un Mac cloud

Tester les limites Unicode du surlignage iOS sur un Mac cloud

Tester les limites Unicode du surlignage iOS sur un Mac cloud

Dans une liste de résultats de recherche, les noms en anglais sont correctement surlignés, mais le surlignage se décale dès qu’un nom contient des signes diacritiques ou qu’un emoji comporte un modificateur de couleur de peau. Le problème vient rarement de la police : il tient généralement au contrat sur les unités d’offset. Le serveur fournit des positions en octets, l’interface attend des positions en UTF-16, et le code métier découpe la chaîne en comptant les caractères Swift. Sur un Mac cloud, commencer par quelques vérifications sur du texte brut permet de cerner le problème plus facilement qu’en multipliant les clics sur le bureau distant.

Cette méthode convient aux extraits de résultats de recherche, au balisage de texte enrichi et aux sélections dans les messages de discussion. Les outils en ligne de commande de Xcode déjà installés suffisent ; aucun service en ligne n’est nécessaire. Sur un nœud physique dédié de XcNode, conservez également les entrées de test avec les informations sur la chaîne d’outils.

Définir d’abord l’unité des offsets

En Swift, String.count compte les grappes de graphèmes étendues, autrement dit les éléments Character ; utf16.count compte les unités de code UTF-16 ; utf8.count compte les octets. Ces trois mesures ne sont pas interchangeables. Un emoji peut réunir un symbole de base, un modificateur de couleur de peau et un caractère de jonction sans chasse : il semble n’être qu’un seul caractère, alors qu’il contient plusieurs unités de code.

Dans les interfaces de Foundation qui manipulent des chaînes, NSRange utilise généralement des unités UTF-16. Ne prenez pas text.count comme limite supérieure d’une plage valide et n’avancez pas directement un String.Index à l’aide d’un entier. La documentation de l’interface doit préciser au minimum l’unité, la version du texte source, le traitement des plages vides et la représentation d’une absence de correspondance.

Une conversion réussie signifie seulement que les indices peuvent être construits. Elle ne garantit pas que les deux extrémités de la plage coïncident avec les limites de caractères complets visibles par l’utilisateur. Le surlignage exige donc une seconde validation.

Toute modification du texte source doit invalider les anciennes plages. N’utilisez pas d’anciens résultats de recherche pour annoter une nouvelle chaîne obtenue après des remplacements, un changement de casse ou des suppressions.

Rejeter les caractères tronqués avec un petit script

Enregistrez le contenu suivant dans highlight-check.swift. La validation traite d’abord les résultats inexistants et les dépassements, puis convertit les indices, avant de vérifier que les deux extrémités se trouvent sur des limites de Character. La vérification de longueur par soustraction évite aussi le débordement d’entier que pourrait provoquer le calcul préalable de la position de départ plus la longueur.

import Foundation

func highlightRange(
    _ value: NSRange,
    in text: String
) -> Range<String.Index>? {
    let total = text.utf16.count
    guard value.location != NSNotFound,
          value.location >= 0,
          value.length >= 0,
          value.location <= total,
          value.length <= total - value.location,
          let range = Range(value, in: text) else {
        return nil
    }

    var boundaries = Set(text.indices)
    boundaries.insert(text.endIndex)
    guard boundaries.contains(range.lowerBound),
          boundaries.contains(range.upperBound) else {
        return nil
    }
    return range
}

let text = "A👩🏽‍💻e\u{301}Z"
let emoji = (text as NSString).range(of: "👩🏽‍💻")
precondition(highlightRange(emoji, in: text) != nil)
precondition(highlightRange(NSRange(location: 1, length: 1),
                           in: text) == nil)

let accent = "e\u{301}"
precondition(highlightRange(NSRange(location: 0, length: 1),
                           in: accent) == nil)
precondition(highlightRange(NSRange(location: 0, length: 2),
                           in: accent) != nil)
precondition(highlightRange(NSRange(location: NSNotFound, length: 0),
                           in: text) == nil)
precondition(highlightRange(NSRange(location: text.utf16.count,
                                   length: 1),
                           in: text) == nil)

Exécutez xcrun swift highlight-check.swift. Un code de sortie nul indique que les assertions ont réussi ; un échec interrompt l’exécution. N’utilisez pas d’options d’optimisation qui désactivent les vérifications. Une fois le script intégré à une cible de tests, remplacez ces assertions par des assertions de test portant un identifiant de cas, afin de retrouver facilement l’entrée en échec.

Le script accepte les plages vides situées sur une limite de caractère, car un éditeur peut en avoir besoin pour représenter un point d’insertion. Les appelants qui ne font que surligner des résultats de recherche doivent rejeter séparément les correspondances de longueur nulle, pour éviter qu’une recherche en boucle ne cesse de progresser.

Conserver les coordonnées du texte source pendant la recherche

Créer une copie simplifiée pour ignorer les signes diacritiques, puis reporter ses offsets sur le texte source, est une erreur courante. Un caractère accentué précomposé et une lettre suivie d’un signe diacritique combinant peuvent, par exemple, représenter le même texte sans avoir nécessairement la même longueur en UTF-16.

Si le produit doit effectivement ignorer la casse ou les signes diacritiques, effectuez la recherche directement dans le texte source : la plage renvoyée restera ainsi rattachée à la chaîne d’origine.

let source = "Cafe\u{301}"
let match = source.range(
    of: "cafe",
    options: [.caseInsensitive, .diacriticInsensitive],
    locale: Locale(identifier: "en_US_POSIX")
)
guard let match else {
    fatalError("Expected a search match")
}
let nsRange = NSRange(match, in: source)
precondition(highlightRange(nsRange, in: source) != nil)
precondition(String(source[match]) == source)

La locale est fixée pour rendre cet exemple reproductible, pas pour fournir une règle universelle de recherche dans toutes les langues. Le choix d’ignorer ou non les signes diacritiques dans les noms, comme le traitement de la casse en turc, doit relever des exigences du produit, puis être couvert par des cas de test distincts. Si la recherche nécessite une segmentation en mots ou une translittération, maintenez une correspondance explicite avec le texte source ; ne déduisez pas les positions à partir de différences de longueur.

Faire des cas d’échec un contrôle de non-régression bloquant

Couvrez au minimum les caractères chinois ordinaires, les paires de substituts, les signes diacritiques combinants, les emojis avec modificateur de couleur de peau, les séquences d’emojis reliées et les séquences de drapeaux. Pour chaque catégorie, vérifiez à la fois une correspondance complète et une plage qui coupe le caractère en son sein. Ajoutez ensuite les plages vides au début et à la fin, les dépassements de fin de chaîne et les résultats inexistants.

Pour chaque plage valide, vérifiez l’aller-retour : après conversion en plage Swift, reconvertissez-la avec NSRange(range, in: text) et exigez un résultat identique. Vérifiez aussi le texte effectivement sélectionné, pour ne pas laisser passer une plage valide qui pointe au mauvais endroit.

Sur un texte long, évitez de parcourir à nouveau toutes les limites de caractères pour chaque correspondance. Vous pouvez construire une seule fois l’ensemble des limites d’un même texte source immuable et le partager entre plusieurs correspondances ; reconstruisez-le lorsque le texte change. Ce cache ne doit pas être réutilisé pour des chaînes différentes.

Limiter la validation de l’interface aux preuves de rendu

Une fois les contrôles sur texte brut validés, ouvrez les cas de recherche fixes dans le simulateur. Vérifiez que le surlignage ne sépare pas les signes diacritiques de leur lettre, qu’aucune séquence d’emoji n’est colorée seulement en partie et que les marques correspondent toujours au texte source après un retour à la ligne. L’inspection visuelle traite les problèmes de rendu ; elle ne remplace pas les assertions sur les plages.

Lorsqu’une recherche asynchrone renvoie ses résultats, vérifiez d’abord la version du texte source associée à la requête, puis appliquez le surlignage. Si le contenu du champ de saisie a changé, ignorez les anciens résultats. Distinguez également la plage de recherche de la plage finalement affichée : si la couche d’affichage doit étendre la plage à des caractères complets, conservez la correspondance d’origine pour les décisions métier et utilisez la plage étendue uniquement pour le rendu.

Le critère de validation final n’est pas « l’application ne plante pas », mais des unités explicites, des plages valides, des limites respectant les caractères complets et un texte sélectionné correct. Ajoutez les chaînes ayant réellement provoqué un échec aux cas de test fixes : à la prochaine modification de l’algorithme de recherche, vous saurez immédiatement quels comportements ont changé.

Questions fréquentes

Une conversion réussie de NSRange garantit-elle un surlignage correct ?

Non. Vérifiez aussi que les deux bornes correspondent à des limites de Character, à partir de text.indices et de endIndex.

Peut-on réutiliser les offsets d’une copie sans accents ?

Pas directement : la transformation peut modifier la longueur UTF-16. Cherchez dans le texte source avec les options prévues ou conservez une table de correspondance.

Louez un Mac dédié dans le cloud pour vos projets de développement

Des nœuds physiques Apple Silicon, pas des machines virtuelles. Louez à la journée, à la semaine, au mois ou au trimestre. Modèles et tarifs affichés en toute transparence ; disponibilité réelle consultable en temps réel dans la console.

Louer un Mac dans le cloud Voir les modèles et les durées de location