NSRange von Swift Range?

175

Problem: NSAttributedString verwendet einen NSRange, während ich einen Swift-String verwende, der Range verwendet

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)
    }
})

Erzeugt den folgenden Fehler:

Fehler: 'Range' kann nicht in 'NSRange' konvertiert werden. attributeString.addAttribute (NSForegroundColorAttributeName, Wert: NSColor.redColor (), Bereich: substringRange)

Jay
quelle
4
Mögliches Duplikat von NSRange zu Range <String.Index>
Suhaib
2
@Suhaib das geht umgekehrt.
Geoff

Antworten:

262

Schnelle StringBereiche und NSStringBereiche sind nicht "kompatibel". Zum Beispiel zählt ein Emoji wie 😄 als ein Swift-Zeichen, aber als zwei NSString Zeichen (ein sogenanntes UTF-16-Ersatzpaar).

Daher führt Ihre vorgeschlagene Lösung zu unerwarteten Ergebnissen, wenn die Zeichenfolge solche Zeichen enthält. Beispiel:

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)

Ausgabe:

😄😄😄Langes Paragra {
} ph say {
    NSColor = "NSCalibratedRGBColorSpace 1 0 0 1";
} ing! {
}}

Wie Sie sehen, wurde "ph say" mit dem Attribut markiert, nicht mit "say".

Da NS(Mutable)AttributedStringletztendlich ein NSStringund ein erforderlich ist NSRange, ist es tatsächlich besser, den angegebenen String NSStringzuerst in einen zu konvertieren . Dann substringRange ist das ein NSRangeund Sie müssen die Bereiche nicht mehr konvertieren:

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)

Ausgabe:

😄😄😄Langer Absatz {
}Sprichwort{
    NSColor = "NSCalibratedRGBColorSpace 1 0 0 1";
}! {
}}

Update für 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)

Update für 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)

Update für Swift 4:

Ab Swift 4 (Xcode 9) bietet die Swift-Standardbibliothek eine Methode zum Konvertieren zwischen Range<String.Index>und NSRange. Eine Umstellung auf NSStringist nicht mehr erforderlich:

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)

Hier substringRangeist ein Range<String.Index>, und das wird in das entsprechende NSRangemit konvertiert

NSRange(substringRange, in: text)
Martin R.
quelle
74
Für alle, die Emoji-Zeichen unter OSX eingeben möchten - Control-Command-Leertaste bringt eine Zeichenauswahl
Jay
2
Dies funktioniert nicht, wenn ich mehr als ein Wort finde und ich nicht sicher bin, wie die gesamte Zeichenfolge übereinstimmen soll. Angenommen, ich erhalte eine Zeichenfolge von einer API zurück und verwende sie in einer anderen Zeichenfolge. Ich möchte, dass die Zeichenfolge aus der API unterstrichen wird. Ich kann nicht garantieren, dass die Teilzeichenfolgen nicht sowohl in der Zeichenfolge der API als auch in der anderen enthalten sind String! Irgendwelche Ideen?
Simonthumper
NSMakeRange Geändert str.substringWithRange (Bereich <String.Index> (Start: str.startIndex, Ende: str.endIndex)) // "Hallo Spielplatz" dies die Änderungen
HariKrishnan.P
(oder) Casting des Strings --- let substring = (String als NSString) .substringWithRange (NSMakeRange (Start, Länge))
HariKrishnan.P
2
Sie erwähnen das Range<String.Index>und NSStringsind nicht kompatibel. Sind ihre Gegenstücke auch nicht kompatibel? Dh sind NSRangeund Stringinkompatibel? Denn eine von Apples API kombiniert speziell die beiden: Übereinstimmungen (in: Optionen: Bereich :)
Sinnvoll
56

Für Fälle wie den von Ihnen beschriebenen habe ich festgestellt, dass dies funktioniert. Es ist relativ kurz und süß:

 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)
Royherma
quelle
11
zugeschriebenString.addAttribute funktioniert nicht mit einem schnellen Bereich
Paludis
7
@Paludis, Sie haben Recht, aber diese Lösung versucht nicht, einen Swift-Bereich zu verwenden. Es wird ein NSRange. strist ein NSStringund gibt daher str.RangeOfString()ein zurück NSRange.
tjpaul
3
Sie können die doppelte Zeichenfolge in Zeile 2 auch entfernen, indem Sie die Zeilen 2 und 3 durch Folgendes ersetzen:let str = attributedString.string as NSString
Jason Moore
2
Dies ist ein Alptraum der Lokalisierung.
Sulthan
29

Die Antworten sind in Ordnung, aber mit Swift 4 können Sie Ihren Code ein wenig vereinfachen:

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

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

Seien Sie vorsichtig, da das Ergebnis der rangeFunktion ausgepackt werden muss.

George Maisuradze
quelle
10

Mögliche Lösung

Swift stellt distance () bereit, das den Abstand zwischen Start und Ende misst, mit dem ein NSRange erstellt werden kann:

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)
        }
})
Jay
quelle
2
Hinweis: Dies kann zu Problemen führen, wenn Zeichen wie Emoji in der Zeichenfolge verwendet werden. Siehe Martins Antwort.
Jay
7

Für mich funktioniert das perfekt:

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
Breno Vinícios
quelle
5

Swift 4:

Klar, ich weiß, dass Swift 4 bereits eine Erweiterung für NSRange hat

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

Ich weiß, dass dieser Init in den meisten Fällen ausreicht. Siehe seine Verwendung:

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

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

Die Konvertierung kann jedoch direkt von Range <String.Index> nach NSRange ohne Swifts String-Instanz erfolgen.

Statt generic init Nutzung des von Ihnen das erfordert Zielparameter als String und wenn Sie nicht haben Ziel Zeichenfolge zur Hand können Sie Konvertierung direkt erstellen

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

oder Sie können die spezielle Erweiterung für Range selbst erstellen

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

Verwendung:

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

oder

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

Swift 5:

Aufgrund der standardmäßigen Migration von Swift-Zeichenfolgen zur UTF-8-Codierung wird die Verwendung von encodedOffsetals veraltet betrachtet und Range kann ohne eine Instanz von String selbst nicht in NSRange konvertiert werden, da zur Berechnung des Offsets die Quellzeichenfolge erforderlich ist in UTF-8 codiert und sollte vor der Berechnung des Offsets in UTF-16 konvertiert werden. Der beste Ansatz ist daher, generisches Init zu verwenden .

Dmitry A.
quelle
Die Verwendung von encodedOffsetwird als schädlich angesehen und veraltet sein .
Martin R
3

Swift 4

Ich denke, es gibt zwei Möglichkeiten.

1. NSRange (Bereich, in :)

2. NSRange (Ort:, Länge :)

Beispielcode:

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))
}

Bildschirmfoto: Geben Sie hier die Bildbeschreibung ein

Den
quelle
Die Verwendung von encodedOffsetwird als schädlich angesehen und veraltet sein .
Martin R
1

Swift 3-Erweiterungsvariante , die vorhandene Attribute beibehält.

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
  }
}
jriskin
quelle
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
}
Orkoden
quelle
0

Ich liebe die Swift-Sprache, aber die Verwendung NSAttributedStringmit einem Swift Range, der nicht kompatibel NSRangeist, hat meinen Kopf zu lange verletzt. Um all diesen Müll zu umgehen, habe ich die folgenden Methoden entwickelt, um eine zurückzugebenNSMutableAttributedString mit den hervorgehobenen Wörtern zurückzugeben, die mit Ihrer Farbe festgelegt wurden.

Dies funktioniert nicht für Emojis. Ändern Sie, wenn Sie müssen.

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
    }
}

Verwendung:

// 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
Brandon A.
quelle
-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)
Jonas
quelle
6
Wie wäre es, wenn Sie Ihre Antwort ein wenig erklären und den Code vorzugsweise richtig formatieren?
SamB