NSRange to Range <String.Index>


258

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

Voglio usare il seguente UITextFieldDelegatemetodo:

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

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

inserisci qui la descrizione dell'immagine


71
Grazie, Apple, per averci fornito la nuova classe Range, ma poi non aver aggiornato nessuna delle classi di utilità della stringa, come NSRegularExpression, per usarle!
puzzl

Risposte:


269

Il NSString versione (al contrario di Swift String) replacingCharacters(in: NSRange, with: NSString)accetta NSRange, quindi una soluzione semplice è quella di convertirla Stringper NSStringprima . I nomi dei metodi delegato e sostitutivo 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)
}

14
Questo non funzionerà se textFieldcontiene caratteri unicode di unità multi-codice come emoji. Dato che si tratta di un campo di immissione, un utente può inserire emoji (😂), altri caratteri della pagina 1 di caratteri di unità multi-codice come i flag (🇪🇸).
zaph,

2
@Zaph: poiché l'intervallo proviene da UITextField, che è scritto in Obj-C contro NSString, sospetto che solo i range validi basati sui caratteri unicode nella stringa sarebbero forniti dal callback delegato, e quindi questo è sicuro da usare , ma non l'ho testato personalmente.
Alex Pretzlav,

2
@Zaph, è vero, il mio punto è che UITextFieldDelegateil textField:shouldChangeCharactersInRange:replacementString:metodo non fornirà un intervallo che inizia o termina all'interno di un carattere unicode, poiché il callback si basa su un utente che immette caratteri dalla tastiera e non è possibile digitare solo una parte di un personaggio unicode emoji. Si noti che se il delegato fosse scritto in Obj-C, avrebbe lo stesso problema.
Alex Pretzlav,

4
Non è la risposta migliore. Il codice più semplice da leggere ma @ martin-r ha la risposta corretta.
Rog,

3
In realtà voi ragazzi dovreste farlo(textField.text as NSString?)?.stringByReplacingCharactersInRange(range, withString: string)
Wanbok Choi,

361

A partire da Swift 4 (Xcode 9), la libreria standard Swift fornisce metodi per la conversione tra intervalli di stringhe Swift ( Range<String.Index>) e NSStringintervalli ( 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: str)!
print(str[r2]) // 🇩🇪

Pertanto la sostituzione del testo nel metodo 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)
        // ...
    }
    // ...
}

(Risposte precedenti per Swift 3 e precedenti :)

A partire da Swift 1.2, String.Indexha un inizializzatore

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

che possono essere utilizzati per convertire NSRangea Range<String.Index>correttamente (compresi tutti i casi di Emojis, indicatori regionali o altri raggruppamenti grafema estesi) senza conversione intermedia ad un 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 tuttoNSRange s sono validi per una determinata stringa Swift.

Il UITextFieldDelegatemetodo delegato 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 di Swift 2 è rangeFromNSRange()stata già fornita da Serhii Yakovenko in questa risposta , la includo 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)) // 🇩🇪

7
Tale codice non funziona correttamente per i caratteri costituiti da più di un punto di codice UTF-16, ad esempio Emoji. - Ad esempio, se il campo di testo contiene il testo "👿" e si elimina quel carattere, rangeè (0,2)perché si NSRangeriferisce ai caratteri UTF-16 in un NSString. Ma conta come un carattere Unicode in una stringa Swift. - let end = advance(start, range.length)dalla risposta a cui viene fatto riferimento si blocca in questo caso con il messaggio di errore "errore irreversibile: impossibile incrementare endIndex".
Martin R,

8
@MattDiPasquale: Certo. La mia intenzione era di rispondere alla domanda alla lettera "Come posso convertire NSRange in Range <String.Index> in Swift" in un modo Unicode sicuro (sperando che qualcuno possa trovarlo utile, cosa che finora non è il caso :(
Martin R

5
Grazie, ancora una volta, per aver svolto il lavoro di Apple per loro. Senza persone come te e Stack Overflow, non avrei mai potuto sviluppare iOS / OS X. La vita è troppo corta.
Womble,

1
@ jiminybob99: Comando-clic su Stringper passare al riferimento API, leggere tutti i metodi e i commenti, quindi provare cose diverse fino a quando non funziona :)
Martin R

1
Perché let from16 = utf16.index(utf16.startIndex, offsetBy: nsRange.location, limitedBy: utf16.endIndex), penso che tu voglia davvero limitarlo utf16.endIndex - 1. Altrimenti, puoi iniziare la fine della stringa.
Michael Tsai,

18

Devi usare al Range<String.Index>posto del classico NSRange. Il modo in cui lo faccio (forse c'è un modo migliore) è prendendo la corda String.Indexe spostandolaadvance .

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)

18

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 advance()funzione. La versione aggiornata è 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)
    }
}

7

Questo è simile alla risposta di Emilie, tuttavia, dal momento che hai chiesto in modo specifico come convertirlo NSRangein Range<String.Index>farebbe qualcosa del genere:

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

}

2
La vista caratteri e la vista UTF16 di una stringa possono avere lunghezze diverse. Questa funzione sta usando gli indici UTF16 (questo è ciò che NSRangeparla, sebbene i suoi componenti siano numeri interi) contro la vista dei caratteri della stringa, che può fallire se usato su una stringa con "caratteri" che impiegano più di un'unità UTF16 per esprimere.
Nate Cook,

6

Un riff sulla grande risposta di @Emilie, non una risposta sostitutiva / concorrente.
(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

Notare l'account degli indici per più unità di codice. La bandiera (LETTERE DI SIMBOLO DELL'INDICATORE REGIONALE) è di 8 byte e (FACCIA CON LE STRADE 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

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

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

 }

2

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)

1

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

NSRangedai NSStringconteggi UTF-16 code-unit s. E Range<String.Index>da Swift Stringè un tipo relativo opaco che fornisce solo operazioni di uguaglianza e navigazione. Questo è un disegno intenzionalmente nascosto.

Anche se Range<String.Index>sembra essere mappato all'offset 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. Rappresentazione interna di SwiftString non è abbastanza definita e non posso fare affidamento su di essa.

NSRange i valori possono essere direttamente mappati String.UTF16View indici. Ma non esiste un metodo per convertirloString.Index .

Swift String.Indexè indice per iterare Swift Characterche è un cluster grapheme Unicode . Quindi, è necessario fornire correttamenteNSRange che seleziona i cluster grapheme corretti. Se si fornisce un intervallo errato come nell'esempio sopra, si produrrà un risultato errato perché non è possibile capire l'intervallo del cluster grapheme corretto.

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

Conversione inversa

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

In Swift 2, return NSRange(location: a.utf16Count, length: b.utf16Count)deve essere modificato inreturn NSRange(location: a.utf16.count, length: b.utf16.count)
Elliot

1

Ho trovato che l'unica soluzione swift2 più pulita è 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 quindi chiamalo da per la funzione 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
}

Ho notato che il mio codice non funzionava più dopo l'ultimo aggiornamento di Swift2. Ho aggiornato la mia risposta per lavorare con Swift2.
Danny Bravo,

0

La documentazione ufficiale di Swift 3.0 beta ha fornito la sua soluzione standard per questa situazione con il titolo String.UTF16View nella sezione UTF16View Elements Match NSString Characters title


Sarà utile se fornirai link nella tua risposta.
m5khan,

0

Nella risposta accettata trovo ingombranti gli optional. Funziona con Swift 3 e sembra non avere problemi con gli 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
}

0
extension StringProtocol where Index == String.Index {

    func nsRange(of string: String) -> NSRange? {
        guard let range = self.range(of: string) else {  return nil }
        return NSRange(range, in: self)
    }
}
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.