[nsstring] NSRange nell'intervallo <String.Index>



Answers

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)) // πŸ‡©πŸ‡ͺ
Question

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)




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)



Un riff sulla grande risposta di @ Emilie, non una sostituzione / risposta in competizione.
(Xcode6-Beta5)

var original    = "πŸ‡ͺπŸ‡ΈπŸ˜‚This is a test"
var replacement = "!"

var startIndex = advance(original.startIndex, 1) // Start at the second character
var endIndex   = advance(startIndex, 2) // point ahead two characters
var range      = Range(start:startIndex, end:endIndex)
var final = original.stringByReplacingCharactersInRange(range, withString:replacement)

println("start index: \(startIndex)")
println("end index:   \(endIndex)")
println("range:       \(range)")
println("original:    \(original)")
println("final:       \(final)")

Produzione:

start index: 4
end index:   7
range:       4..<7
original:    πŸ‡ͺπŸ‡ΈπŸ˜‚This is a test
final:       πŸ‡ͺπŸ‡Έ!his is a test

Si noti l'account degli indici per più unità di codice. Il flag (INDICATORE REGIONALE SIMBOLO LETTERE ES) è 8 byte e il (FRONTE CON I RACCOLTI DI GIOIA) è di 4 byte. (In questo caso particolare risulta che il numero di byte è lo stesso per le rappresentazioni UTF-8, UTF-16 e UTF-32.)

Avvolgendolo in una funzione:

func replaceString(#string:String, #with:String, #start:Int, #length:Int) ->String {
    var startIndex = advance(original.startIndex, start) // Start at the second character
    var endIndex   = advance(startIndex, length) // point ahead two characters
    var range      = Range(start:startIndex, end:endIndex)
    var final = original.stringByReplacingCharactersInRange(range, withString: replacement)
    return final
}

var newString = replaceString(string:original, with:replacement, start:1, length:2)
println("newString:\(newString)")

Produzione:

newString: !his is a test



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



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
}



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
}



Related