NSRange de Swift Range?

176

Problema: NSAttributedString toma un NSRange mientras uso una cadena Swift 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)
    }
})

Produce el siguiente error:

error: 'Range' no es convertible a 'NSRange' attributeString.addAttribute (NSForegroundColorAttributeName, valor: NSColor.redColor (), range: substringRange)

Arrendajo
fuente
44
Posible duplicado de NSRange to Range <String.Index>
Suhaib
2
@Suhaib que va al revés.
Geoff

Respuestas:

262

Los Stringrangos rápidos y los NSStringrangos no son "compatibles". Por ejemplo, un emoji como 😄 cuenta como un personaje Swift, pero como dos NSString caracteres (un par sustituto llamado UTF-16).

Por lo tanto, su solución sugerida producirá resultados inesperados si la cadena contiene dichos caracteres. Ejemplo:

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)

Salida:

😄😄😄Paragra larga {
} ph decir {
    NSColor = "NSCalibratedRGBColorSpace 1 0 0 1";
}¡En g!{
}

Como puede ver, "ph say" ha sido marcado con el atributo, no "diciendo".

Como en NS(Mutable)AttributedStringúltima instancia requiere una NSStringy una NSRange, en realidad es mejor convertir NSStringprimero la cadena dada . Entonces el substringRange es un NSRangey ya no tienes que convertir los rangos:

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)

Salida:

😄😄😄Párrafo largo {
}diciendo{
    NSColor = "NSCalibratedRGBColorSpace 1 0 0 1";
}! {
}

Actualización para 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)

Actualización para 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)

Actualización para Swift 4:

A partir de Swift 4 (Xcode 9), la biblioteca estándar de Swift proporciona un método para convertir entre Range<String.Index>y NSRange. La conversión a NSStringya no es necesaria:

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)

Aquí substringRangehay un Range<String.Index>, y que se convierte al correspondiente NSRangecon

NSRange(substringRange, in: text)
Martin R
fuente
74
Para cualquiera que quiera escribir caracteres emoji en OSX - Control-Command-barra espaciadora trae un selector de caracteres
Jay
2
Esto no funciona si estoy haciendo coincidir más de una palabra, y no estoy seguro de cuál es la cadena completa. Digamos que estoy recuperando una cadena de una API y la uso dentro de otra cadena, y quiero que la cadena de la API esté subrayada, no puedo garantizar que las subcadenas no estén tanto en la cadena de la API como en la otra ¡cuerda! ¿Algunas ideas?
simonthumper
NSMakeRange cambió str.substringWithRange (Range <String.Index> (start: str.startIndex, end: str.endIndex)) // "Hola, área de juegos", esto cambia
HariKrishnan.P
(o) lanzar la cadena --- let substring = (cadena como NSString) .substringWithRange (NSMakeRange (inicio, longitud))
HariKrishnan.P
2
Mencionas eso Range<String.Index>y NSStringno son compatibles. ¿Son sus contrapartes también incompatibles? Es decir, son NSRangey Stringincompatibles? Porque una de las API de Apple combina específicamente las dos: coincidencias (en: opciones: rango :)
Senseful
57

Para casos como el que describiste, encontré que esto funciona. Es relativamente corto y dulce:

 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
fuente
11
tribuString.addAttribute
77
@Paludis, tienes razón, pero esta solución no está intentando usar un rango Swift. Está usando un NSRange. stres un NSStringy por lo tanto str.RangeOfString()devuelve un NSRange.
tjpaul
3
También puede eliminar la cadena duplicada en la línea 2 reemplazando las líneas 2 y 3 con:let str = attributedString.string as NSString
Jason Moore
2
Esta es una pesadilla de localización.
Sulthan
29

Las respuestas están bien, pero con Swift 4 podría simplificar un poco su código:

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

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

Tenga cuidado, ya que el resultado de la rangefunción debe desenvolverse.

George Maisuradze
fuente
10

Solución posible

Swift proporciona la distancia () que mide la distancia entre el inicio y el final que se puede usar para crear 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)
        }
})
Arrendajo
fuente
2
Nota: Esto puede romperse si se usan caracteres como emoji en la cadena. Vea la respuesta de Martin.
Jay
7

Para mí esto funciona perfectamente:

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

Swift 4:

Claro, sé que Swift 4 ya tiene una extensión para NSRange

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

Sé que en la mayoría de los casos este init es suficiente. Ver su uso:

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

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

Pero la conversión se puede hacer directamente desde Range <String.Index> a NSRange sin la instancia de Strift de Swift.

En lugar del uso de inicio genérico que requiere de usted el parámetro de destino como Cadena y si no tiene una cadena de destino a mano, puede crear la conversión directamente

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

o puede crear la extensión especializada para 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))) //  "🐶🦇🐱"
}

o

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

Swift 5:

Debido a la migración de las cadenas Swift a la codificación UTF-8 de forma predeterminada, el uso de encodedOffsetse considera obsoleto y Range no se puede convertir a NSRange sin una instancia de String, porque para calcular el desplazamiento necesitamos la cadena fuente que es codificado en UTF-8 y debe convertirse a UTF-16 antes de calcular el desplazamiento. Entonces, el mejor enfoque, por ahora, es usar init genérico .

Dmitry A.
fuente
El uso de encodedOffsetse considera perjudicial y quedará en desuso .
Martin R
3

Swift 4

Creo que hay dos formas.

1. NSRange (rango, en:)

2. NSRange (ubicación :, longitud:)

Código de muestra:

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 pantalla: ingrese la descripción de la imagen aquí

Guarida
fuente
El uso de encodedOffsetse considera perjudicial y quedará en desuso .
Martin R
1

Variante de extensión Swift 3 que conserva los 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
fuente
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
fuente
0

Me encanta el lenguaje Swift, pero usarlo NSAttributedStringcon un Swift Rangeque no es compatible me NSRangeha dolido mucho la cabeza. Entonces, para evitar toda esa basura, ideé los siguientes métodos para devolver un NSMutableAttributedStringcon las palabras resaltadas establecidas con su color.

Esto no funciona para emojis. Modifique si debe hacerlo.

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
fuente
-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
fuente
66
¿Qué tal si explicamos un poco su respuesta y, preferiblemente, formateamos el código correctamente?
SamB