NSRange от Swift Range?

177

Проблема: NSAttributedString принимает NSRange, в то время как я использую строку Swift, которая использует 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)
    }
})

Выдает следующую ошибку:

ошибка: «Диапазон» не может быть преобразован в «NSRange» attribute.String.addAttribute (NSForegroundColorAttributeName, значение: NSColor.redColor (), диапазон: substringRange)

сойка
источник
4
Возможный дубликат NSRange для диапазона <String.Index>
Сухайб
2
@Suhaib, что происходит наоборот.
Джефф

Ответы:

262

Быстрые Stringдиапазоны и NSStringдиапазоны не являются «совместимыми». Например, смайлик типа 😄 считается одним символом Свифта, но двумя NSString символами (так называемая суррогатная пара UTF-16).

Поэтому предложенное вами решение даст неожиданные результаты, если строка содержит такие символы. Пример:

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)

Вывод:

ParaДлительная парагра {
} ph сказать {
    NSColor = "NSCalibratedRGBColorSpace 1 0 0 1";
} ИНГ! {
}

Как видите, «ph say» был помечен атрибутом, а не «say».

Так как в NS(Mutable)AttributedStringконечном итоге требуются an NSStringи an NSRange, на самом деле лучше преобразовать данную строку в NSStringпервую. Тогда substringRange это NSRangeи вам больше не нужно конвертировать диапазоны:

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)

Вывод:

ParagraphДлинный абзац {
} {Говоря
    NSColor = "NSCalibratedRGBColorSpace 1 0 0 1";
}! {
}

Обновление для 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)

Обновление для 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)

Обновление для Swift 4:

Начиная с Swift 4 (Xcode 9), стандартная библиотека Swift предоставляет метод для преобразования между Range<String.Index>и NSRange. Преобразование в NSStringбольше не нужно:

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)

Вот substringRangeэто Range<String.Index>, и что превращают в соответствующее NSRangeс

NSRange(substringRange, in: text)
Мартин Р
источник
74
Для тех, кто хочет печатать символы эмодзи на OSX - Control-Command-пробел вызывает окно выбора персонажа
Jay
2
Это не сработает, если я сопоставляю более одного слова, и я не уверен, какова вся строка для сопоставления. Скажем, я получаю строку обратно из API и использую ее в другой строке, и я хочу, чтобы строка из API была подчеркнута, я не могу гарантировать, что подстроки не будут находиться как в строке из API, так и в другой строка! Любые идеи?
Simonthumper
NSMakeRange Изменено str.substringWithRange (Range <String.Index> (начало: str.startIndex, конец: str.endIndex)) // «Привет, детская площадка» это изменения
HariKrishnan.P
(или) приведение строки --- let substring = (строка как NSString) .substringWithRange (NSMakeRange (начало, длина))
HariKrishnan.P
2
Вы упоминаете об этом Range<String.Index>и NSStringне совместимы. Их аналоги также несовместимы? То есть NSRangeи Stringнесовместимы? Потому что один из API Apple специально объединяет два: совпадения (в: options: range :)
Senseful
57

Для случаев, подобных тому, который вы описали, я обнаружил, что это работает. Это относительно коротко и сладко:

 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
источник
11
attribuString.addAttribute
7
@Paludis, вы правы, но это решение не пытается использовать диапазон Swift. Это использует NSRange. strявляется NSStringи, следовательно, str.RangeOfString()возвращает NSRange.
tjpaul
3
Вы также можете удалить дублирующую строку в строке 2, заменив строки 2 и 3 на:let str = attributedString.string as NSString
Джейсон Мур
2
Это кошмар локализации.
Sulthan
29

Ответы хороши, но с Swift 4 вы можете немного упростить ваш код:

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

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

Будьте осторожны, так как результат rangeфункции должен быть развернут.

Георгий Майсурадзе
источник
10

Возможное решение

Swift предоставляет distance (), который измеряет расстояние между началом и концом, которое можно использовать для создания 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
Примечание: это может сломаться, если использовать такие символы, как эмодзи в строке - см. Ответ Мартина.
Джей
7

Для меня это работает отлично:

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

Свифт 4:

Конечно, я знаю, что Swift 4 уже имеет расширение для NSRange

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

Я знаю, что в большинстве случаев этого достаточно. Смотрите его использование:

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

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

Но преобразование может быть выполнено непосредственно из Range <String.Index> в NSRange без экземпляра Swift String.

Вместо общего использования init, которое требует от вас целевого параметра как String, и если у вас нет целевой строки под рукой, вы можете создать преобразование напрямую

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

или вы можете создать специализированное расширение для самого Range

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

Использование:

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

или

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

Свифт 5:

Из-за миграции строк Swift в кодировку UTF-8 по умолчанию использование encodedOffsetсчитается устаревшим, и Range нельзя преобразовать в NSRange без экземпляра самой String, поскольку для вычисления смещения нам нужна исходная строка, которая является закодированы в UTF-8 и должны быть преобразованы в UTF-16 перед вычислением смещения. Так что лучший подход на данный момент - использовать общий init .

Дмитрий А.
источник
Использование encodedOffsetявляется считается вредным и будет устаревшим .
Мартин Р
3

Swift 4

Я думаю, что есть два пути.

1. NSRange (диапазон, в:)

2. NSRange (местоположение :, длина:)

Образец кода:

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

Скриншот: введите описание изображения здесь

логово
источник
Использование encodedOffsetявляется считается вредным и будет устаревшим .
Мартин Р
1

Вариант расширения Swift 3, который сохраняет существующие атрибуты.

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
источник
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
источник
0

Я люблю язык Swift, но использование NSAttributedStringс Swift Range, которое не совместимо с NSRange, заставило мою голову болеть слишком долго. Таким образом, чтобы обойти весь этот мусор, я разработал следующие методы, чтобы вернуть NSMutableAttributedStringс выделенными словами, установленными с вашим цветом.

Это не работает для смайликов. Изменить, если необходимо.

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

Использование:

// 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
Как насчет того, чтобы немного объяснить свой ответ и желательно правильно отформатировать код?
SamB