nsstring - NSRange nell'intervallo<String.Index>





swift ios8 (11)


Ecco il mio miglior sforzo. Ma questo non può controllare o rilevare argomenti di input errati.

extension String {
    /// :r: Must correctly select proper UTF-16 code-unit range. Wrong range will produce wrong result.
    public func convertRangeFromNSRange(r:NSRange) -> Range<String.Index> {
        let a   =   (self as NSString).substringToIndex(r.location)
        let b   =   (self as NSString).substringWithRange(r)

        let n1  =   distance(a.startIndex, a.endIndex)
        let n2  =   distance(b.startIndex, b.endIndex)

        let i1  =   advance(startIndex, n1)
        let i2  =   advance(i1, n2)

        return  Range<String.Index>(start: i1, end: i2)
    }
}

let s   =   "πŸ‡ͺπŸ‡ΈπŸ˜‚"
println(s[s.convertRangeFromNSRange(NSRange(location: 4, length: 2))])      //  Proper range. Produces correct result.
println(s[s.convertRangeFromNSRange(NSRange(location: 0, length: 4))])      //  Proper range. Produces correct result.
println(s[s.convertRangeFromNSRange(NSRange(location: 0, length: 2))])      //  Improper range. Produces wrong result.
println(s[s.convertRangeFromNSRange(NSRange(location: 0, length: 1))])      //  Improper range. Produces wrong result.

Risultato.

πŸ˜‚
πŸ‡ͺπŸ‡Έ
πŸ‡ͺπŸ‡Έ
πŸ‡ͺπŸ‡Έ

Dettagli

NSRange di NSString conta unità di codice UTF-16. E Range<String.Index> di Swift String è un tipo relativo opaco che fornisce solo operazioni di uguaglianza e navigazione. Questo è design intenzionalmente nascosto.

Anche se la Range<String.Index> sembra essere mappata Range<String.Index> di unità di codice UTF-16, questo è solo un dettaglio di implementazione, e non sono riuscito a trovare alcuna menzione su alcuna garanzia. Ciò significa che i dettagli di implementazione possono essere modificati in qualsiasi momento. La rappresentazione interna di Swift String non è ben definita e non posso farvi affidamento.

NSRange valori NSRange possono essere mappati direttamente agli indici String.UTF16View . Ma non esiste alcun metodo per convertirlo in String.Index .

Swift String.Index è indice per iterare Swift Character che è un String.Index Unicode . Quindi, è necessario fornire NSRange appropriato che seleziona i NSRange grapheme corretti. Se fornisci un intervallo errato come nell'esempio sopra, produrrà risultati errati perché non è stato possibile calcolare l'intervallo del graftema corretto.

Se esiste una garanzia che lo String.Index sia l' offset dell'unità di codice UTF-16, il problema diventa semplice. Ma è improbabile che accada.

Conversione inversa

In ogni caso la conversione inversa può essere fatta con precisione.

extension String {
    /// O(1) if `self` is optimised to use UTF-16.
    /// O(n) otherwise.
    public func convertRangeToNSRange(r:Range<String.Index>) -> NSRange {
        let a   =   substringToIndex(r.startIndex)
        let b   =   substringWithRange(r)

        return  NSRange(location: a.utf16Count, length: b.utf16Count)
    }
}
println(convertRangeToNSRange(s.startIndex..<s.endIndex))
println(convertRangeToNSRange(s.startIndex.successor()..<s.endIndex))

Risultato.

(0,6)
(4,2)

Come posso convertire NSRange in Range<String.Index> in Swift?

Voglio utilizzare il seguente metodo UITextFieldDelegate :

    func textField(textField: UITextField!,
        shouldChangeCharactersInRange range: NSRange,
        replacementString string: String!) -> Bool {

textField.text.stringByReplacingCharactersInRange(???, withString: string)




A partire da Swift 4 (Xcode 9), la libreria standard Swift fornisce metodi per la conversione tra gli intervalli di stringhe Swift ( Range<String.Index> ) e NSString ( NSRange ). Esempio:

let str = "aπŸ‘ΏbπŸ‡©πŸ‡ͺc"
let r1 = str.range(of: "πŸ‡©πŸ‡ͺ")!

// String range to NSRange:
let n1 = NSRange(r1, in: str)
print((str as NSString).substring(with: n1)) // πŸ‡©πŸ‡ͺ

// NSRange back to String range:
let r2 = Range(n1, in: text)!
print(str.substring(with: r2)) // πŸ‡©πŸ‡ͺ

Pertanto la sostituzione del testo nel metodo del delegato del campo di testo può ora essere eseguita come

func textField(_ textField: UITextField,
               shouldChangeCharactersIn range: NSRange,
               replacementString string: String) -> Bool {

    if let oldString = textField.text {
        let newString = oldString.replacingCharacters(in: Range(range, in: oldString)!,
                                                      with: string)
        // ...
    }
    // ...
}

(Le risposte precedenti per Swift 3 e precedenti :)

A partire da Swift 1.2, String.Index ha un inizializzatore

init?(_ utf16Index: UTF16Index, within characters: String)

che può essere utilizzato per convertire NSRange in Range<String.Index> correttamente (inclusi tutti i casi di Emojis, Indicatori regionali o altri cluster grafi estesi) senza conversione intermedia a una NSString :

extension String {
    func rangeFromNSRange(nsRange : NSRange) -> Range<String.Index>? {
        let from16 = advance(utf16.startIndex, nsRange.location, utf16.endIndex)
        let to16 = advance(from16, nsRange.length, utf16.endIndex)
        if let from = String.Index(from16, within: self),
            let to = String.Index(to16, within: self) {
                return from ..< to
        }
        return nil
    }
}

Questo metodo restituisce un intervallo di stringhe opzionale perché non tutti gli NSRange sono validi per una determinata stringa Swift.

Il metodo delegato UITextFieldDelegate può quindi essere scritto come

func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool {

    if let swRange = textField.text.rangeFromNSRange(range) {
        let newString = textField.text.stringByReplacingCharactersInRange(swRange, withString: string)
        // ...
    }
    return true
}

La conversione inversa è

extension String {
    func NSRangeFromRange(range : Range<String.Index>) -> NSRange {
        let utf16view = self.utf16
        let from = String.UTF16View.Index(range.startIndex, within: utf16view) 
        let to = String.UTF16View.Index(range.endIndex, within: utf16view)
        return NSMakeRange(from - utf16view.startIndex, to - from)
    }
}

Un semplice test:

let str = "aπŸ‘ΏbπŸ‡©πŸ‡ͺc"
let r1 = str.rangeOfString("πŸ‡©πŸ‡ͺ")!

// String range to NSRange:
let n1 = str.NSRangeFromRange(r1)
println((str as NSString).substringWithRange(n1)) // πŸ‡©πŸ‡ͺ

// NSRange back to String range:
let r2 = str.rangeFromNSRange(n1)!
println(str.substringWithRange(r2)) // πŸ‡©πŸ‡ͺ

Aggiornamento per Swift 2:

La versione Swift 2 di rangeFromNSRange() è già stata fornita da Serhii Yakovenko in questa risposta , la sto includendo qui per completezza:

extension String {
    func rangeFromNSRange(nsRange : NSRange) -> Range<String.Index>? {
        let from16 = utf16.startIndex.advancedBy(nsRange.location, limit: utf16.endIndex)
        let to16 = from16.advancedBy(nsRange.length, limit: utf16.endIndex)
        if let from = String.Index(from16, within: self),
            let to = String.Index(to16, within: self) {
                return from ..< to
        }
        return nil
    }
}

La versione Swift 2 di NSRangeFromRange() è

extension String {
    func NSRangeFromRange(range : Range<String.Index>) -> NSRange {
        let utf16view = self.utf16
        let from = String.UTF16View.Index(range.startIndex, within: utf16view)
        let to = String.UTF16View.Index(range.endIndex, within: utf16view)
        return NSMakeRange(utf16view.startIndex.distanceTo(from), from.distanceTo(to))
    }
}

Aggiornamento per Swift 3 (Xcode 8):

extension String {
    func nsRange(from range: Range<String.Index>) -> NSRange {
        let from = range.lowerBound.samePosition(in: utf16)
        let to = range.upperBound.samePosition(in: utf16)
        return NSRange(location: utf16.distance(from: utf16.startIndex, to: from),
                       length: utf16.distance(from: from, to: to))
    }
}

extension String {
    func range(from nsRange: NSRange) -> Range<String.Index>? {
        guard
            let from16 = utf16.index(utf16.startIndex, offsetBy: nsRange.location, limitedBy: utf16.endIndex),
            let to16 = utf16.index(utf16.startIndex, offsetBy: nsRange.location + nsRange.length, limitedBy: utf16.endIndex),
            let from = from16.samePosition(in: self),
            let to = to16.samePosition(in: self)
            else { return nil }
        return from ..< to
    }
}

Esempio:

let str = "aπŸ‘ΏbπŸ‡©πŸ‡ͺc"
let r1 = str.range(of: "πŸ‡©πŸ‡ͺ")!

// String range to NSRange:
let n1 = str.nsRange(from: r1)
print((str as NSString).substring(with: n1)) // πŸ‡©πŸ‡ͺ

// NSRange back to String range:
let r2 = str.range(from: n1)!
print(str.substring(with: r2)) // πŸ‡©πŸ‡ͺ



Nella risposta accettata trovo le opzioni ingombranti. Funziona con Swift 3 e sembra non avere problemi con le emoji.

func textField(_ textField: UITextField, 
      shouldChangeCharactersIn range: NSRange, 
      replacementString string: String) -> Bool {

  guard let value = textField.text else {return false} // there may be a reason for returning true in this case but I can't think of it
  // now value is a String, not an optional String

  let valueAfterChange = (value as NSString).replacingCharacters(in: range, with: string)
  // valueAfterChange is a String, not an optional String

  // now do whatever processing is required

  return true  // or false, as required
}



È necessario utilizzare Range<String.Index> anziché il classico NSRange . Il modo in cui lo faccio (forse c'è un modo migliore) è prendere String.Index della stringa e spostarlo con advance .

Non so quale intervallo stai cercando di sostituire, ma facciamo finta di voler sostituire i primi 2 caratteri.

var start = textField.text.startIndex // Start at the string's start index
var end = advance(textField.text.startIndex, 2) // Take start index and advance 2 characters forward
var range: Range<String.Index> = Range<String.Index>(start: start,end: end)

textField.text.stringByReplacingCharactersInRange(range, withString: string)



In Swift 2.0 assumendo func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool { :

var oldString = textfield.text!
let newRange = oldString.startIndex.advancedBy(range.location)..<oldString.startIndex.advancedBy(range.location + range.length)
let newString = oldString.stringByReplacingCharactersInRange(newRange, withString: string)



Ho trovato che la soluzione più semplice swift2 è quella di creare una categoria su NSRange:

extension NSRange {
    func stringRangeForText(string: String) -> Range<String.Index> {
        let start = string.startIndex.advancedBy(self.location)
        let end = start.advancedBy(self.length)
        return Range<String.Index>(start: start, end: end)
    }
}

E poi chiamalo da per la funzione del delegato del campo di testo:

func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool {
    let range = range.stringRangeForText(textField.text)
    let output = textField.text.stringByReplacingCharactersInRange(range, withString: string)

    // your code goes here....

    return true
}



func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool {

       let strString = ((textField.text)! as NSString).stringByReplacingCharactersInRange(range, withString: string)

 }



Questo è simile alla risposta di Emilie, tuttavia, poiché hai chiesto specificamente come convertire NSRange in Range<String.Index> avresti fatto qualcosa di simile a questo:

func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool {

     let start = advance(textField.text.startIndex, range.location) 
     let end = advance(start, range.length) 
     let swiftRange = Range<String.Index>(start: start, end: end) 
     ...

}



La versione NSString (al contrario di Swift String) di replacingCharacters(in: NSRange, with: NSString) accetta un NSRange , quindi una soluzione semplice consiste nel convertire prima String in NSString . I nomi dei delegati e dei metodi di sostituzione sono leggermente diversi in Swift 3 e 2, quindi a seconda di quale Swift stai usando:

Swift 3.0

func textField(_ textField: UITextField,
               shouldChangeCharactersIn range: NSRange,
               replacementString string: String) -> Bool {

  let nsString = textField.text as NSString?
  let newString = nsString?.replacingCharacters(in: range, with: string)
}

Swift 2.x

func textField(textField: UITextField,
               shouldChangeCharactersInRange range: NSRange,
               replacementString string: String) -> Bool {

    let nsString = textField.text as NSString?
    let newString = nsString?.stringByReplacingCharactersInRange(range, withString: string)
}



Questa risposta di Martin R sembra essere corretta perché rappresenta Unicode.

Tuttavia al momento del post (Swift 1) il suo codice non viene compilato in Swift 2.0 (Xcode 7), perché hanno rimosso la funzione advance() . La versione aggiornata è qui sotto:

Swift 2

extension String {
    func rangeFromNSRange(nsRange : NSRange) -> Range<String.Index>? {
        let from16 = utf16.startIndex.advancedBy(nsRange.location, limit: utf16.endIndex)
        let to16 = from16.advancedBy(nsRange.length, limit: utf16.endIndex)
        if let from = String.Index(from16, within: self),
            let to = String.Index(to16, within: self) {
                return from ..< to
        }
        return nil
    }
}

Swift 3

extension String {
    func rangeFromNSRange(nsRange : NSRange) -> Range<String.Index>? {
        if let from16 = utf16.index(utf16.startIndex, offsetBy: nsRange.location, limitedBy: utf16.endIndex),
            let to16 = utf16.index(from16, offsetBy: nsRange.length, limitedBy: utf16.endIndex),
            let from = String.Index(from16, within: self),
            let to = String.Index(to16, within: self) {
                return from ..< to
        }
        return nil
    }
}

Swift 4

extension String {
    func rangeFromNSRange(nsRange : NSRange) -> Range<String.Index>? {
        return Range(nsRange, in: self)
    }
}



String intervalli di String Swift e gli intervalli NSString non sono "compatibili". Ad esempio, un'emoji come πŸ˜„ conta come un carattere Swift, ma come due caratteri NSString (una cosiddetta coppia di surrogati 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 paragra{
}ph say{
    NSColor = "NSCalibratedRGBColorSpace 1 0 0 1";
}ing!{
}

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

Poiché NS(Mutable)AttributedString richiede in definitiva un NSString e un NSRange , in realtà è meglio convertire prima la stringa data in NSString . Quindi substringRange è un NSRange e non è più necessario 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:

πŸ˜„πŸ˜„πŸ˜„Long paragraph {
}saying{
    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 la conversione tra Range<String.Index> e NSRange . La conversione in NSString non è 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)

Qui substringRange è un Range<String.Index> e viene convertito nel corrispondente NSRange con

NSRange(substringRange, in: text)




nsstring swift ios8 nsrange