NSRange od Swift Range?

175

Problem: NSAttributedString pobiera NSRange, gdy używam Swift String, który używa 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)
    }
})

Powoduje następujący błąd:

błąd: „Zakres” nie jest konwertowany na „NSRange” attributeString.addAttribute (NSForegroundColorAttributeName, wartość: NSColor.redColor (), zakres: substringRange)

Sójka
źródło
4
Możliwy duplikat NSRange to Range <String.Index>
Suhaib
2
@Suhaib to odwrotnie.
geoff

Odpowiedzi:

262

Szybkie Stringzakresy i NSStringzakresy nie są „zgodne”. Na przykład emoji, takie jak 😄, liczy się jako jeden znak Swift, ale jako dwa NSString znaki (tak zwana para zastępcza UTF-16).

Dlatego sugerowane rozwiązanie przyniesie nieoczekiwane wyniki, jeśli ciąg zawiera takie znaki. Przykład:

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)

Wynik:

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

Jak widać, „ph say” zostało oznaczone atrybutem, a nie „mówiąc”.

Ponieważ NS(Mutable)AttributedStringostatecznie wymaga an NSStringi NSRange, w rzeczywistości lepiej jest przekonwertować dany ciąg na NSStringpierwszy. Wtedy substringRange jest an NSRangei nie musisz już konwertować zakresów:

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)

Wynik:

😄😄😄Długi akapit {
}powiedzenie{
    NSColor = "NSCalibratedRGBColorSpace 1 0 0 1";
}! {
}

Aktualizacja dla 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)

Aktualizacja dla 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)

Aktualizacja dla Swift 4:

Począwszy od Swift 4 (Xcode 9), standardowa biblioteka Swift zapewnia metodę konwersji między Range<String.Index>i NSRange. Konwersja na NSStringnie jest już konieczna:

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)

Tutaj substringRangejest Range<String.Index>, i to jest konwertowane na odpowiadające NSRangez

NSRange(substringRange, in: text)
Martin R.
źródło
74
Dla każdego, kto chce pisać znaki emoji w systemie OSX - klawisz Control-Command-spacja wyświetla selektor znaków
Jay
2
To nie działa, jeśli dopasowuję więcej niż jedno słowo i nie jestem pewien, jaki jest cały pasujący ciąg. Powiedzmy, że otrzymuję ciąg z powrotem z interfejsu API i używam go w innym ciągu i chcę, aby ciąg z interfejsu API był podkreślony, nie mogę zagwarantować, że podciągi nie będą znajdować się zarówno w ciągu z interfejsu API, jak i drugim strunowy! Jakieś pomysły?
simonthumper
NSMakeRange Changed str.substringWithRange (Range <String.Index> (start: str.startIndex, end: str.endIndex)) // "Witaj, zabaw" to zmiany
HariKrishnan.P
(lub) rzutowanie ciągu --- let substring = (string jako NSString) .substringWithRange (NSMakeRange (start, length))
HariKrishnan.P
2
Wspomniałeś o tym Range<String.Index>i NSStringnie jesteś zgodny. Czy ich odpowiedniki również są niekompatybilne? To znaczy są NSRangei Stringniekompatybilne? Ponieważ jeden z API firmy Apple łączy w szczególności dwa: dopasowania (w: opcje: zakres :)
Senseful
56

W przypadkach takich jak ten, który opisałeś, okazało się, że działa. Jest stosunkowo krótki i słodki:

 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
źródło
11
AttributeString.addAttribute nie będzie działać z szybkim zakresem
Paludis,
7
@Paludis, masz rację, ale to rozwiązanie nie jest próbą użycia zakresu Swift. Używa NSRange. strjest NSStringi dlatego str.RangeOfString()zwraca NSRange.
tjpaul
3
Możesz również usunąć zduplikowany ciąg w linii 2, zastępując wiersze 2 i 3:let str = attributedString.string as NSString
Jason Moore
2
To koszmar lokalizacji.
Sulthan
29

Odpowiedzi są w porządku, ale dzięki Swift 4 możesz nieco uprościć kod:

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

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

Bądź ostrożny, ponieważ wynik rangefunkcji musi zostać rozpakowany.

George Maisuradze
źródło
10

Możliwe rozwiązanie

Swift zapewnia distance (), która mierzy odległość między początkiem a końcem, której można użyć do utworzenia 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)
        }
})
Sójka
źródło
2
Uwaga: może to się zepsuć, jeśli w ciągu znaków używane są znaki takie jak emoji - zobacz odpowiedź Martina.
Jay
7

U mnie to działa idealnie:

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
źródło
5

Swift 4:

Jasne, wiem, że Swift 4 ma już rozszerzenie dla NSRange

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

Wiem, że w większości przypadków ten init jest wystarczający. Zobacz jego zastosowanie:

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

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

Ale konwersję można wykonać bezpośrednio z Range <String.Index> na NSRange bez wystąpienia String języka Swift.

Zamiast ogólnego użycia init, które wymaga od ciebie parametru docelowego jako String, a jeśli nie masz pod ręką docelowego ciągu, możesz bezpośrednio utworzyć konwersję

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

lub możesz utworzyć specjalistyczne rozszerzenie dla samego zakresu

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

Stosowanie:

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

lub

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

Swift 5:

Ze względu na migrację ciągów Swift do kodowania UTF-8 domyślnie, użycie encodedOffsetjest uważane za przestarzałe, a Range nie można przekonwertować na NSRange bez wystąpienia samego ciągu, ponieważ do obliczenia przesunięcia potrzebujemy ciągu źródłowego, który jest zakodowany w UTF-8 i powinien zostać przekonwertowany na UTF-16 przed obliczeniem przesunięcia. Na razie najlepszym rozwiązaniem jest więc użycie generycznego init .

Dmitry A.
źródło
Użycie programu encodedOffsetjest uważane za szkodliwe i zostanie wycofane .
Martin R
3

Szybki 4

Myślę, że są dwa sposoby.

1. NSRange (zakres, w:)

2. NSRange (lokalizacja :, długość:)

Przykładowy kod:

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

Zrzut ekranu: wprowadź opis obrazu tutaj

Legowisko
źródło
Użycie programu encodedOffsetjest uważane za szkodliwe i zostanie wycofane .
Martin R
1

Wariant rozszerzenia Swift 3, który zachowuje istniejące atrybuty.

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
źródło
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
źródło
0

Uwielbiam język Swift, ale używanie go NSAttributedStringz językiem Swift, Rangektóry nie jest kompatybilny NSRange, sprawiało, że bolała mnie głowa zbyt długo. Aby ominąć te wszystkie śmieci, opracowałem następujące metody zwracania znaku NSMutableAttributedStringz wyróżnionymi słowami ustawionymi w Twoim kolorze.

To nie działa w przypadku emoji. Zmodyfikuj, jeśli musisz.

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

Stosowanie:

// 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.
źródło
-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
źródło
6
Może trochę wyjaśnisz swoją odpowiedź, a najlepiej odpowiednio sformatujesz kod?
SamB