NSRange da Swift Range?

176

Problema: NSAttributedString usa um NSRange enquanto estou usando uma Swift String que usa 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)
    }
})

Produz o seguinte erro:

erro: 'Range' não é convertível em 'NSRange' attributeString.addAttribute (NSForegroundColorAttributeName, valor: NSColor.redColor (), range: substringRange)

Jay
fonte
4
Possível duplicata do NSRange no intervalo <String.Index>
Suhaib 08/08
2
@ Suhaib que está indo ao contrário.
26519 geoff

Respostas:

262

Os Stringintervalos e os NSStringintervalos rápidos não são "compatíveis". Por exemplo, um emoji como 😄 conta como um caractere Swift, mas como dois NSString caracteres (o chamado par substituto UTF-16).

Portanto, sua solução sugerida produzirá resultados inesperados se a sequência contiver esses caracteres. Exemplo:

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)

Resultado:

ParLonga paragra {
} ph diz {
    NSColor = "NSCalibratedRGBColorSpace 1 0 0 1";
} ing! {
}

Como você vê, "ph say" foi marcado com o atributo, não "dizendo".

Como, em NS(Mutable)AttributedStringúltima análise, requer um NSStringe um NSRange, é realmente melhor converter a sequência fornecida em NSStringprimeiro. Então o substringRange é um NSRangee você não precisa mais converter os intervalos:

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)

Resultado:

Paragraph Parágrafo longo {
}dizendo{
    NSColor = "NSCalibratedRGBColorSpace 1 0 0 1";
}! {
}

Atualização para o 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)

Atualização para o 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)

Atualização para o Swift 4:

A partir do Swift 4 (Xcode 9), a biblioteca padrão do Swift fornece um método para converter entre Range<String.Index>e NSRange. A conversão para NSStringnão é mais necessária:

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)

Aqui substringRangeestá um Range<String.Index>, e que é convertido no correspondente NSRangecom

NSRange(substringRange, in: text)
Martin R
fonte
74
Para quem deseja digitar caracteres emoji no OSX - Control-Command-barra de espaço traz um seletor de caráter
Jay
2
Isso não funciona se eu estiver combinando mais de uma palavra e não tiver certeza de qual é a sequência inteira. Digamos que estou recebendo uma string de volta de uma API e usando-a dentro de outra string e quero que a string da API seja sublinhada, não posso garantir que as substrings não estejam na string da API e na outra. corda! Alguma ideia?
simonthumper
NSMakeRange Alterado str.substringWithRange (Intervalo <String.Index> (início: str.startIndex, final: str.endIndex)) // "Olá, playground", isto é, as mudanças
HariKrishnan.P
(ou) lançando a corda --- deixe substring = (string como NSString) .substringWithRange (NSMakeRange (início, comprimento))
HariKrishnan.P
2
Você mencionou isso Range<String.Index>e NSStringnão é compatível. Os colegas também são incompatíveis? Ou seja, são NSRangee Stringincompatíveis? Porque uma das APIs da Apple combina especificamente os dois: fósforos (em: opções: intervalo :)
Sensuous
57

Para casos como o que você descreveu, achei que funcionava. É relativamente curto e agradável:

 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
fonte
11
attributedString.addAttribute não funcionará com uma gama rápida
Paludis
7
@Paludis, você está correto, mas esta solução não está tentando usar um intervalo Swift. Está usando um NSRange. stré um NSStringe, portanto, str.RangeOfString()retorna um NSRange.
tjpaul 9/09/16
3
Você também pode remover a seqüência de caracteres duplicada na linha 2 substituindo as linhas 2 e 3 por:let str = attributedString.string as NSString
Jason Moore
2
Este é um pesadelo de localização.
Sulthan
29

As respostas são boas, mas com o Swift 4 você pode simplificar um pouco o seu código:

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

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

Seja cauteloso, pois o resultado da rangefunção deve ser desembrulhado.

George Maisuradze
fonte
10

Solução possível

Swift fornece distance (), que mede a distância entre o início e o final que pode ser usada para criar um 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)
        }
})
Jay
fonte
2
Nota: Isso pode ser interrompido se você usar caracteres como emoji na string - consulte a resposta de Martin.
Jay
7

Para mim, isso funciona perfeitamente:

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
fonte
5

Swift 4:

Claro, eu sei que o Swift 4 já tem uma extensão para o NSRange

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

Eu sei que na maioria dos casos esse init é suficiente. Veja seu uso:

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

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

Mas a conversão pode ser feita diretamente de Range <String.Index> para NSRange sem a instância String de Swift.

Em vez do uso genérico do init , que exige de você o parâmetro target como String e, se você não tem a string alvo em mãos, pode criar a conversão diretamente

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

ou você pode criar a extensão especializada para o próprio Range

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

Uso:

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:

Devido à migração das strings Swift para a codificação UTF-8 por padrão, o uso de encodedOffseté considerado obsoleto e o Range não pode ser convertido em NSRange sem uma instância da própria String, porque, para calcular o deslocamento, precisamos da string de origem que é codificado em UTF-8 e deve ser convertido em UTF-16 antes de calcular o deslocamento. Portanto, a melhor abordagem, por enquanto, é usar init genérico .

Dmitry A.
fonte
O uso de encodedOffseté considerado prejudicial e será preterido .
Martin R
3

Swift 4

Eu acho que existem duas maneiras.

1. NSRange (intervalo, em:)

2. NSRange (localização :, comprimento:)

Código de amostra:

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

Captura de tela: insira a descrição da imagem aqui

Den
fonte
O uso de encodedOffseté considerado prejudicial e será preterido .
Martin R
1

Variante de extensão Swift 3 que preserva os atributos existentes.

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
fonte
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
fonte
0

Eu amo a linguagem Swift, mas usar NSAttributedStringuma Swift Rangeque não é compatível NSRangefez minha cabeça doer por muito tempo. Então, para contornar todo esse lixo, criei os seguintes métodos para retornar um NSMutableAttributedStringcom as palavras destacadas definidas com a sua cor.

Isso não funciona para emojis. Modifique se necessário.

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

Uso:

// 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
fonte
-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
fonte
6
Que tal explicar um pouco sua resposta e, de preferência, formatar o código corretamente?
SamB 22/08/16