NSRange di Swift Range?


176

Problema: NSAttributedString accetta un intervallo NS mentre sto utilizzando una stringa Swift che utilizza 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 il seguente errore:

errore: 'Range' non è convertibile in 'NSRange' attribuitoString.addAttribute (NSForegroundColorAttributeName, valore: NSColor.redColor (), intervallo: substringRange)


4
Possibile duplicato di NSRange su Range <String.Index>
Suhaib

2
@Suhaib che sta andando al contrario.
geoff,

Risposte:


262

Le Stringgamme veloci e le NSStringgamme non sono "compatibili". Ad esempio, un'emoji come 😄 conta come un personaggio Swift, ma come due NSString personaggi (una cosiddetta coppia surrogata UTF-16).

Pertanto la soluzione suggerita produrrà risultati imprevisti se la stringa contiene tali caratteri. Esempio:

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)

Produzione:

😄😄😄Long paragrafo {
} ph dire {
    NSColor = "NSCalibratedRGBColorSpace 1 0 0 1";
} Ing! {
}

Come vedi, "ph dire" è stato contrassegnato con l'attributo, non "dire".

Poiché alla NS(Mutable)AttributedStringfine richiede an NSStringe an NSRange, in realtà è meglio convertire prima la stringa data NSString. Quindi substringRange è un NSRangee non devi più convertire gli intervalli:

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)

Produzione:

😄😄😄Lungo paragrafo {
}detto{
    NSColor = "NSCalibratedRGBColorSpace 1 0 0 1";
}! {
}

Aggiornamento per 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)

Aggiornamento per 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)

Aggiornamento per Swift 4:

A partire da Swift 4 (Xcode 9), la libreria standard Swift fornisce il metodo per convertire tra Range<String.Index>e NSRange. La conversione in NSStringnon è più necessaria:

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)

Ecco substringRangeun Range<String.Index>, e che viene convertito nel corrispondente NSRangecon

NSRange(substringRange, in: text)

74
Per chiunque voglia digitare caratteri emoji su OSX - Control-Command-space bar fa apparire un selettore di caratteri
Jay,

2
Questo non funziona se sto abbinando più di una parola e non sono sicuro quale sia l'intera stringa da abbinare. Supponiamo che sto recuperando una stringa da un'API e la usi all'interno di un'altra stringa, e voglio che la stringa dall'API sia sottolineata, non posso garantire che le sottostringhe non si trovino nella stringa sia dall'API che dall'altra corda! Qualche idea?
simonthumper,

NSMakeRange Modificato str.substringWithRange (Range <String.Index> (inizio: str.startIndex, fine: str.endIndex)) // "Ciao, parco giochi" questo cambia
HariKrishnan.P

(o) casting della stringa --- let substring = (stringa come NSString) .substringWithRange (NSMakeRange (inizio, lunghezza))
HariKrishnan.P

2
Lo dici Range<String.Index>e NSStringnon sono compatibili. Anche le loro controparti sono incompatibili? Cioè sono NSRangee Stringincompatibili? Perché una delle API di Apple combina in modo specifico le due: partite (in: opzioni: intervallo :)
Senso

57

Per casi come quello che hai descritto, ho scoperto che funzionava. È relativamente breve e dolce:

 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)


7
@Paludis, hai ragione, ma questa soluzione non sta tentando di utilizzare un intervallo Swift. Sta usando un NSRange. strè un NSStringe quindi str.RangeOfString()restituisce un NSRange.
tjpaul,

3
Puoi anche rimuovere la stringa duplicata nella riga 2 sostituendo le righe 2 e 3 con:let str = attributedString.string as NSString
Jason Moore,

2
Questo è un incubo di localizzazione.
Sulthan,

29

Le risposte vanno bene, ma con Swift 4 potresti semplificare un po 'il tuo codice:

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

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

Sii cauto, poiché il risultato della rangefunzione deve essere scartato.


10

Possibile soluzione

Swift fornisce la distanza () che misura la distanza tra inizio e fine che può essere utilizzata per creare un intervallo NS:

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
Nota: questo può interrompersi se si usano caratteri come emoji nella stringa - Vedi la risposta di Martin.
Jay,

7

Per me questo funziona perfettamente:

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

Swift 4:

Certo, so che Swift 4 ha già un'estensione per NSRange

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

So che nella maggior parte dei casi questo init è sufficiente. Vedi il suo utilizzo:

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

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

Ma la conversione può essere effettuata direttamente da Range <String.Index> a NSRange senza l'istanza String di Swift.

Invece di un utilizzo di init generico che richiede da te il parametro target come String e se non hai una stringa target a portata di mano puoi creare direttamente la conversione

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

oppure puoi creare l'estensione specializzata per Range stesso

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:

A causa della migrazione delle stringhe Swift nella codifica UTF-8 per impostazione predefinita, l'utilizzo di encodedOffsetè considerato obsoleto e Range non può essere convertito in NSRange senza un'istanza di String stessa, perché per calcolare l'offset è necessaria la stringa di origine che è codificato in UTF-8 e deve essere convertito in UTF-16 prima di calcolare l'offset. Quindi il miglior approccio, per ora, è usare l' init generico .


L'uso di encodedOffsetè considerato dannoso e sarà deprecato .
Martin R

3

Swift 4

Penso che ci siano due modi.

1. NSRange (range, in:)

2. NSRange (posizione :, lunghezza:)

Codice di esempio:

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

Immagine dello schermo: inserisci qui la descrizione dell'immagine


L'uso di encodedOffsetè considerato dannoso e sarà deprecato .
Martin R

1

Swift 3 Extension Variante che conserva gli attributi esistenti.

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

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
}

0

Adoro il linguaggio Swift, ma l'utilizzo NSAttributedStringcon uno Swift Rangeche non è compatibile mi NSRangeha fatto male alla testa per troppo tempo. Quindi per aggirare tutta quella spazzatura ho escogitato i seguenti metodi per restituire una NSMutableAttributedStringcon le parole evidenziate impostate con il tuo colore.

Questo non funziona per gli emoji. Modifica se è necessario.

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

-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
Che ne dici di spiegare un po 'la tua risposta e preferibilmente di formattare correttamente il codice?
SamB
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.