NSRange de Swift Range?


176

Problème: NSAttributedString prend un NSRange pendant que j'utilise une Swift String qui utilise Range

let text = "Long paragraph saying something goes here!"
let textRange = text.startIndex..<text.endIndex
let attributedString = NSMutableAttributedString(string: text)

text.enumerateSubstringsInRange(textRange, options: NSStringEnumerationOptions.ByWords, { (substring, substringRange, enclosingRange, stop) -> () in

    if (substring == "saying") {
        attributedString.addAttribute(NSForegroundColorAttributeName, value: NSColor.redColor(), range: substringRange)
    }
})

Produit l'erreur suivante:

erreur: 'Range' n'est pas convertible en 'NSRange' attribuéString.addAttribute (NSForegroundColorAttributeName, value: NSColor.redColor (), range: substringRange)



2
@Suhaib c'est l'inverse.
geoff

Réponses:


262

Les Stringgammes et NSStringgammes Swift ne sont pas "compatibles". Par exemple, un emoji comme 😄 compte pour un caractère Swift, mais comme deux NSString caractères (une paire de substituts UTF-16).

Par conséquent, votre solution suggérée produira des résultats inattendus si la chaîne contient de tels caractères. Exemple:

let text = "😄😄😄Long paragraph saying!"
let textRange = text.startIndex..<text.endIndex
let attributedString = NSMutableAttributedString(string: text)

text.enumerateSubstringsInRange(textRange, options: NSStringEnumerationOptions.ByWords, { (substring, substringRange, enclosingRange, stop) -> () in
    let start = distance(text.startIndex, substringRange.startIndex)
    let length = distance(substringRange.startIndex, substringRange.endIndex)
    let range = NSMakeRange(start, length)

    if (substring == "saying") {
        attributedString.addAttribute(NSForegroundColorAttributeName, value: NSColor.redColor(), range: range)
    }
})
println(attributedString)

Production:

😄😄😄Long paragra {
} ph dis {
    NSColor = "NSCalibratedRGBColorSpace 1 0 0 1";
} ing! {
}

Comme vous le voyez, "ph say" a été marqué avec l'attribut, pas "dire".

Étant donné que NS(Mutable)AttributedStringnécessite finalement un NSStringet un NSRange, il est en fait préférable de convertir la chaîne donnée en NSStringpremier. Ensuite, le substringRange est un NSRangeet vous n'avez plus à convertir les plages:

let text = "😄😄😄Long paragraph saying!"
let nsText = text as NSString
let textRange = NSMakeRange(0, nsText.length)
let attributedString = NSMutableAttributedString(string: nsText)

nsText.enumerateSubstringsInRange(textRange, options: NSStringEnumerationOptions.ByWords, { (substring, substringRange, enclosingRange, stop) -> () in

    if (substring == "saying") {
        attributedString.addAttribute(NSForegroundColorAttributeName, value: NSColor.redColor(), range: substringRange)
    }
})
println(attributedString)

Production:

😄😄😄Long paragraphe {
}en disant{
    NSColor = "NSCalibratedRGBColorSpace 1 0 0 1";
}! {
}

Mise à jour pour Swift 2:

let text = "😄😄😄Long paragraph saying!"
let nsText = text as NSString
let textRange = NSMakeRange(0, nsText.length)
let attributedString = NSMutableAttributedString(string: text)

nsText.enumerateSubstringsInRange(textRange, options: .ByWords, usingBlock: {
    (substring, substringRange, _, _) in

    if (substring == "saying") {
        attributedString.addAttribute(NSForegroundColorAttributeName, value: NSColor.redColor(), range: substringRange)
    }
})
print(attributedString)

Mise à jour pour Swift 3:

let text = "😄😄😄Long paragraph saying!"
let nsText = text as NSString
let textRange = NSMakeRange(0, nsText.length)
let attributedString = NSMutableAttributedString(string: text)

nsText.enumerateSubstrings(in: textRange, options: .byWords, using: {
    (substring, substringRange, _, _) in

    if (substring == "saying") {
        attributedString.addAttribute(NSForegroundColorAttributeName, value: NSColor.red, range: substringRange)
    }
})
print(attributedString)

Mise à jour pour Swift 4:

Depuis Swift 4 (Xcode 9), la bibliothèque standard Swift fournit une méthode pour convertir entre Range<String.Index>et NSRange. La conversion en NSStringn'est plus nécessaire:

let text = "😄😄😄Long paragraph saying!"
let attributedString = NSMutableAttributedString(string: text)

text.enumerateSubstrings(in: text.startIndex..<text.endIndex, options: .byWords) {
    (substring, substringRange, _, _) in
    if substring == "saying" {
        attributedString.addAttribute(.foregroundColor, value: NSColor.red,
                                      range: NSRange(substringRange, in: text))
    }
}
print(attributedString)

Voici substringRangeun Range<String.Index>, et qui est converti en le correspondant NSRangeavec

NSRange(substringRange, in: text)

74
Pour tous ceux qui souhaitent taper des caractères emoji sur OSX - La barre d'espace Contrôle-Commande fait apparaître un sélecteur de personnage
Jay

2
Cela ne fonctionne pas si je fais correspondre plus d'un mot, et je ne sais pas quelle est la chaîne entière à faire correspondre. Disons que je récupère une chaîne d'une API et que je l'utilise dans une autre chaîne, et que je souhaite que la chaîne de l'API soit soulignée, je ne peux pas garantir que les sous-chaînes ne seront pas à la fois dans la chaîne de l'API et dans l'autre chaîne! Des idées?
simonthumper

NSMakeRange Changed str.substringWithRange (Range <String.Index> (start: str.startIndex, end: str.endIndex)) // "Bonjour, terrain de jeu"
voici

(ou) casting de la chaîne --- let substring = (string as NSString) .substringWithRange (NSMakeRange (start, length))
HariKrishnan.P

2
Vous mentionnez cela Range<String.Index>et NSStringn'êtes pas compatible. Leurs homologues sont-ils également incompatibles? Ie sont NSRangeet Stringincompatibles? Parce que l'une des API d'Apple combine spécifiquement les deux: matches (dans: options: range :)
Sensible

57

Pour des cas comme celui que vous avez décrit, j'ai trouvé que cela fonctionnait. C'est relativement court et doux:

 let attributedString = NSMutableAttributedString(string: "follow the yellow brick road") //can essentially come from a textField.text as well (will need to unwrap though)
 let text = "follow the yellow brick road"
 let str = NSString(string: text) 
 let theRange = str.rangeOfString("yellow")
 attributedString.addAttribute(NSForegroundColorAttributeName, value: UIColor.yellowColor(), range: theRange)

11
attribuéString.addAttribute ne fonctionnera pas avec un Range Swift
Paludis

7
@Paludis, vous avez raison, mais cette solution ne tente pas d'utiliser une gamme Swift. Il utilise un fichier NSRange. strest un NSStringet str.RangeOfString()renvoie donc un NSRange.
tjpaul le

3
Vous pouvez également supprimer la chaîne en double à la ligne 2 en remplaçant les lignes 2 et 3 par:let str = attributedString.string as NSString
Jason Moore

2
C'est un cauchemar de localisation.
Sulthan

29

Les réponses sont bonnes, mais avec Swift 4, vous pouvez simplifier un peu votre code:

let text = "Test string"
let substring = "string"

let substringRange = text.range(of: substring)!
let nsRange = NSRange(substringRange, in: text)

Soyez prudent, car le résultat de la rangefonction doit être déballé.


10

Solution possible

Swift fournit la distance () qui mesure la distance entre le début et la fin qui peut être utilisée pour créer un NSRange:

let text = "Long paragraph saying something goes here!"
let textRange = text.startIndex..<text.endIndex
let attributedString = NSMutableAttributedString(string: text)

text.enumerateSubstringsInRange(textRange, options: NSStringEnumerationOptions.ByWords, { (substring, substringRange, enclosingRange, stop) -> () in
    let start = distance(text.startIndex, substringRange.startIndex)
    let length = distance(substringRange.startIndex, substringRange.endIndex)
    let range = NSMakeRange(start, length)

//    println("word: \(substring) - \(d1) to \(d2)")

        if (substring == "saying") {
            attributedString.addAttribute(NSForegroundColorAttributeName, value: NSColor.redColor(), range: range)
        }
})

2
Remarque: cela peut casser si vous utilisez des caractères comme emoji dans la chaîne - Voir la réponse de Martin.
Jay

7

Pour moi, cela fonctionne parfaitement:

let font = UIFont.systemFont(ofSize: 12, weight: .medium)
let text = "text"
let attString = NSMutableAttributedString(string: "exemple text :)")

attString.addAttributes([.font: font], range:(attString.string as NSString).range(of: text))

label.attributedText = attString

5

Swift 4:

Bien sûr, je sais que Swift 4 a déjà une extension pour NSRange

public init<R, S>(_ region: R, in target: S) where R : RangeExpression,
    S : StringProtocol, 
    R.Bound == String.Index, S.Index == String.Index

Je sais que dans la plupart des cas, cette init est suffisante. Voir son utilisation:

let string = "Many animals here: 🐶🦇🐱 !!!"

if let range = string.range(of: "🐶🦇🐱"){
     print((string as NSString).substring(with: NSRange(range, in: string))) //  "🐶🦇🐱"
 }

Mais la conversion peut être effectuée directement de Range <String.Index> à NSRange sans l'instance String de Swift.

Au lieu d'une utilisation d' initialisation générique qui nécessite de votre part le paramètre cible en tant que chaîne et si vous n'avez pas de chaîne cible à portée de main, vous pouvez créer une conversion directement

extension NSRange {
    public init(_ range:Range<String.Index>) {
        self.init(location: range.lowerBound.encodedOffset,
              length: range.upperBound.encodedOffset -
                      range.lowerBound.encodedOffset) }
    }

ou vous pouvez créer l'extension spécialisée pour Range lui-même

extension Range where Bound == String.Index {
    var nsRange:NSRange {
    return NSRange(location: self.lowerBound.encodedOffset,
                     length: self.upperBound.encodedOffset -
                             self.lowerBound.encodedOffset)
    }
}

Usage:

let string = "Many animals here: 🐶🦇🐱 !!!"
if let range = string.range(of: "🐶🦇🐱"){
    print((string as NSString).substring(with: NSRange(range))) //  "🐶🦇🐱"
}

ou

if let nsrange = string.range(of: "🐶🦇🐱")?.nsRange{
    print((string as NSString).substring(with: nsrange)) //  "🐶🦇🐱"
}

Swift 5:

En raison de la migration des chaînes Swift vers le codage UTF-8 par défaut, l'utilisation de encodedOffsetest considérée comme obsolète et Range ne peut pas être converti en NSRange sans une instance de String elle-même, car pour calculer le décalage, nous avons besoin de la chaîne source qui est encodé en UTF-8 et il doit être converti en UTF-16 avant de calculer le décalage. La meilleure approche, pour l'instant, est donc d'utiliser init générique .


L'utilisation de encodedOffsetest considérée comme nuisible et sera obsolète .
Martin R

3

Swift 4

Je pense qu'il y a deux façons.

1. NSRange (plage, en:)

2. NSRange (emplacement :, longueur:)

Exemple de code:

let attributedString = NSMutableAttributedString(string: "Sample Text 12345", attributes: [.font : UIFont.systemFont(ofSize: 15.0)])

// NSRange(range, in: )
if let range = attributedString.string.range(of: "Sample")  {
    attributedString.addAttribute(.foregroundColor, value: UIColor.orange, range: NSRange(range, in: attributedString.string))
}

// NSRange(location: , length: )
if let range = attributedString.string.range(of: "12345") {
    attributedString.addAttribute(.foregroundColor, value: UIColor.green, range: NSRange(location: range.lowerBound.encodedOffset, length: range.upperBound.encodedOffset - range.lowerBound.encodedOffset))
}

Capture d'écran: entrez la description de l'image ici


L'utilisation de encodedOffsetest considérée comme nuisible et sera obsolète .
Martin R

1

Variante d'extension Swift 3 qui préserve les attributs existants.

extension UILabel {
  func setLineHeight(lineHeight: CGFloat) {
    guard self.text != nil && self.attributedText != nil else { return }
    var attributedString = NSMutableAttributedString()

    if let attributedText = self.attributedText {
      attributedString = NSMutableAttributedString(attributedString: attributedText)
    } else if let text = self.text {
      attributedString = NSMutableAttributedString(string: text)
    }

    let style = NSMutableParagraphStyle()
    style.lineSpacing = lineHeight
    style.alignment = self.textAlignment
    let str = NSString(string: attributedString.string)

    attributedString.addAttribute(NSParagraphStyleAttributeName,
                                  value: style,
                                  range: str.range(of: str as String))
    self.attributedText = attributedString
  }
}

0
func formatAttributedStringWithHighlights(text: String, highlightedSubString: String?, formattingAttributes: [String: AnyObject]) -> NSAttributedString {
    let mutableString = NSMutableAttributedString(string: text)

    let text = text as NSString         // convert to NSString be we need NSRange
    if let highlightedSubString = highlightedSubString {
        let highlightedSubStringRange = text.rangeOfString(highlightedSubString) // find first occurence
        if highlightedSubStringRange.length > 0 {       // check for not found
            mutableString.setAttributes(formattingAttributes, range: highlightedSubStringRange)
        }
    }

    return mutableString
}

0

J'adore le langage Swift, mais l'utiliser NSAttributedStringavec un Swift Rangequi n'est pas compatible avec NSRangem'a fait mal à la tête pendant trop longtemps. Donc, pour contourner toutes ces ordures, j'ai conçu les méthodes suivantes pour renvoyer un NSMutableAttributedStringavec les mots en surbrillance définis avec votre couleur.

Cela ne fonctionne pas pour les emojis. Modifiez si vous le devez.

extension String {
    func getRanges(of string: String) -> [NSRange] {
        var ranges:[NSRange] = []
        if contains(string) {
            let words = self.components(separatedBy: " ")
            var position:Int = 0
            for word in words {
                if word.lowercased() == string.lowercased() {
                    let startIndex = position
                    let endIndex = word.characters.count
                    let range = NSMakeRange(startIndex, endIndex)
                    ranges.append(range)
                }
                position += (word.characters.count + 1) // +1 for space
            }
        }
        return ranges
    }
    func highlight(_ words: [String], this color: UIColor) -> NSMutableAttributedString {
        let attributedString = NSMutableAttributedString(string: self)
        for word in words {
            let ranges = getRanges(of: word)
            for range in ranges {
                attributedString.addAttributes([NSForegroundColorAttributeName: color], range: range)
            }
        }
        return attributedString
    }
}

Usage:

// The strings you're interested in
let string = "The dog ran after the cat"
let words = ["the", "ran"]

// Highlight words and get back attributed string
let attributedString = string.highlight(words, this: .yellow)

// Set attributed string
label.attributedText = attributedString

-3
let text:String = "Hello Friend"

let searchRange:NSRange = NSRange(location:0,length: text.characters.count)

let range:Range`<Int`> = Range`<Int`>.init(start: searchRange.location, end: searchRange.length)

6
Que diriez-vous d'expliquer un peu votre réponse, et de préférence de formater le code correctement?
SamB
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.