NSRange to Range <String.Index>

258

¿Cómo puedo convertir NSRangea Range<String.Index>en Swift?

Quiero usar el siguiente UITextFieldDelegatemétodo:

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

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

ingrese la descripción de la imagen aquí

János
fuente
71
¡Gracias, Apple, por darnos la nueva clase Range, pero luego no actualizar ninguna de las clases de utilidad de cadena, como NSRegularExpression, para usarlas!
rompecabezas

Respuestas:

269

La NSStringversión (a diferencia de Swift String) de replacingCharacters(in: NSRange, with: NSString)acepta un NSRange, por lo que una solución simple es convertir Stringa NSStringprimero . Los nombres de los métodos de delegado y reemplazo son ligeramente diferentes en Swift 3 y 2, por lo que, dependiendo de qué Swift esté 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)
}
Alex Pretzlav
fuente
14
Esto no funcionará si textFieldcontiene caracteres unicode de unidad de código múltiple como emoji. Dado que es un campo de entrada, un usuario puede ingresar emoji (😂), otros caracteres de la página 1 de caracteres de unidades de múltiples códigos, como banderas (🇪🇸).
zaph
2
@Zaph: dado que el rango proviene del UITextField, que está escrito en Obj-C contra NSString, sospecho que la devolución de llamada delegada solo proporcionará rangos válidos basados ​​en los caracteres unicode en la cadena, por lo que esto es seguro de usar , pero no lo he probado personalmente.
Alex Pretzlav
2
@Zaph, eso es cierto, mi punto es que UITextFieldDelegateel textField:shouldChangeCharactersInRange:replacementString:método no proporcionará un rango que comience o termine dentro de un carácter unicode, ya que la devolución de llamada se basa en que un usuario ingresa caracteres desde el teclado, y no es posible escribir solo una parte de un personaje emoji unicode. Tenga en cuenta que si el delegado se escribiera en Obj-C, tendría el mismo problema.
Alex Pretzlav
44
No es la mejor respuesta. El código es más fácil de leer pero @ martin-r tiene la respuesta correcta.
Rog
3
En realidad ustedes deberían hacer esto(textField.text as NSString?)?.stringByReplacingCharactersInRange(range, withString: string)
Wanbok Choi
361

A partir de Swift 4 (Xcode 9), la biblioteca estándar de Swift proporciona métodos para convertir entre rangos de cadenas Swift ( Range<String.Index>) y NSStringrangos ( NSRange). Ejemplo:

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]) // 🇩🇪

Por lo tanto, el reemplazo de texto en el método de delegado de campo de texto ahora se puede hacer como

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

(Respuestas anteriores para Swift 3 y anteriores :)

A partir de Swift 1.2, String.Indextiene un inicializador

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

que se puede utilizar para convertir NSRangea Range<String.Index>correctamente (incluyendo todos los casos de Emojis, Indicadores Regionales u otras agrupaciones de grafema prolongados) sin conversión 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
    }
}

Este método devuelve un rango de cadena opcional porque no todos los NSRanges son válidos para una cadena Swift determinada.

El UITextFieldDelegatemétodo delegado se puede escribir como

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 conversión inversa es

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

Una prueba simple:

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)) // 🇩🇪

Actualización para Swift 2:

La versión de Swift 2 rangeFromNSRange()ya fue dada por Serhii Yakovenko en esta respuesta , la incluyo aquí para completar:

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 versión Swift 2 de NSRangeFromRange()es

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

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

Ejemplo:

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)) // 🇩🇪
Martin R
fuente
77
Ese código no funciona correctamente para los caracteres que consisten en más de un punto de código UTF-16, por ejemplo, Emojis. - Como ejemplo, si el campo de texto contiene el texto "👿" y elimina ese carácter, rangees (0,2)porque se NSRangerefiere a los caracteres UTF-16 en un NSString. Pero cuenta como un personaje Unicode en una cadena Swift. - let end = advance(start, range.length)de la respuesta referenciada se bloquea en este caso con el mensaje de error "error fatal: no se puede incrementar endIndex".
Martin R
8
@MattDiPasquale: Claro. Mi intención era responder la pregunta literal "¿Cómo puedo convertir NSRange a Range <String.Index> en Swift" de una manera segura para Unicode (esperando que alguien pueda encontrarlo útil, que no es el caso hasta ahora :(
Martin R
55
Gracias, una vez más, por hacer el trabajo de Apple por ellos. Sin personas como tú y Stack Overflow, no hay forma de que esté haciendo el desarrollo de iOS / OS X. La vida es demasiado corta.
Womble
1
@ jiminybob99: Comando-clic Stringpara saltar a la referencia de la API, leer todos los métodos y comentarios, luego probar diferentes cosas hasta que funcione :)
Martin R
1
Porque let from16 = utf16.index(utf16.startIndex, offsetBy: nsRange.location, limitedBy: utf16.endIndex), creo que realmente quieres limitarlo utf16.endIndex - 1. De lo contrario, puede comenzar desde el final de la cadena.
Michael Tsai
18

Necesita usar en Range<String.Index>lugar del clásico NSRange. La forma en que lo hago (tal vez hay una mejor manera) es tomando la cuerda String.Indexy moviéndola advance.

No sé qué rango está tratando de reemplazar, pero supongamos que desea reemplazar los primeros 2 caracteres.

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)
Emilie
fuente
18

Esta respuesta de Martin R parece ser correcta porque representa Unicode.

Sin embargo, en el momento de la publicación (Swift 1) su código no se compila en Swift 2.0 (Xcode 7), porque eliminaron la advance()función. La versión actualizada está abajo:

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)
    }
}
Serhii Yakovenko
fuente
7

Sin embargo, esto es similar a la respuesta de Emilie, ya que usted preguntó específicamente cómo convertirlo NSRangea Range<String.Index>usted haría algo como esto:

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

}
James Jones
fuente
2
La vista de caracteres y la vista UTF16 de una cadena pueden tener diferentes longitudes. Esta función está utilizando índices UTF16 (eso es lo que NSRangehabla, aunque sus componentes son enteros) contra la vista de caracteres de la cadena, que puede fallar cuando se usa en una cadena con "caracteres" que requieren más de una unidad UTF16 para expresarse.
Nate Cook, el
6

Un riff sobre la gran respuesta de @Emilie, no una respuesta de reemplazo / competencia.
(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)")

Salida:

start index: 4
end index:   7
range:       4..<7
original:    🇪🇸😂This is a test
final:       🇪🇸!his is a test

Observe que los índices representan múltiples unidades de código. La bandera (LETRAS DE SÍMBOLO DEL INDICADOR REGIONAL ES) tiene 8 bytes y la (CARA CON LÁGRIMAS DE ALEGRÍA) tiene 4 bytes. (En este caso particular, resulta que el número de bytes es el mismo para las representaciones UTF-8, UTF-16 y UTF-32).

Envolviéndolo en una función:

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

Salida:

newString: !his is a test
zaph
fuente
4
func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool {

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

 }
Darshan Panchal
fuente
2

En Swift 2.0 asumiendo 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)
Brenden
fuente
1

Aquí está mi mejor esfuerzo. Pero esto no puede verificar o detectar un argumento de entrada incorrecto.

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.

Resultado.

😂
🇪🇸
🇪🇸
🇪🇸

Detalles

NSRangede los NSStringrecuentos de unidades de código UTF-16 . Y Range<String.Index>de Swift Stringes un opaco tipo relativo que proporciona solo operaciones de igualdad y navegación. Este es un diseño intencionalmente oculto.

Aunque Range<String.Index>parece estar asignado al desplazamiento de la unidad de código UTF-16, eso es solo un detalle de implementación, y no pude encontrar ninguna mención sobre ninguna garantía. Eso significa que los detalles de implementación se pueden cambiar en cualquier momento. La representación interna de Swift Stringno está bastante definida, y no puedo confiar en ella.

NSRangelos valores se pueden asignar directamente a los String.UTF16Viewíndices. Pero no hay ningún método para convertirlo enString.Index .

Swift String.Indexes un índice para iterar Swift, Characterque es un clúster de grafema Unicode . Luego, debe proporcionar el adecuado NSRangeque selecciona los grupos de grafemas correctos. Si proporciona un rango incorrecto como en el ejemplo anterior, producirá un resultado incorrecto porque no se pudo determinar el rango adecuado del grupo de grafemas.

Si hay una garantía de que el String.Index es UTF-16-unidad de desplazamiento de código, a continuación, se convierte en un problema sencillo. Pero es poco probable que suceda.

Conversión inversa

De todos modos, la conversión inversa se puede hacer con precisión.

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

Resultado.

(0,6)
(4,2)
Eonil
fuente
En Swift 2, return NSRange(location: a.utf16Count, length: b.utf16Count)debe cambiarse areturn NSRange(location: a.utf16.count, length: b.utf16.count)
Elliot
1

He encontrado que la única solución más limpia de swift2 es crear una categoría en 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)
    }
}

Y luego llame desde la función de delegado de campo de texto:

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
}
Danny Bravo
fuente
Noté que mi código ya no funcionaba después de la última actualización de Swift2. He actualizado mi respuesta para trabajar con Swift2.
Danny Bravo
0

La documentación oficial de Swift 3.0 beta ha proporcionado su solución estándar para esta situación bajo el título String.UTF16View en la sección UTF16View Elements Match NSString Characters title

usuario6511559
fuente
Será útil si proporciona enlaces en su respuesta.
m5khan
0

En la respuesta aceptada encuentro los opcionales engorrosos. Esto funciona con Swift 3 y parece no tener problemas con los emojis.

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
}
Murray Sagal
fuente
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)
    }
}
zheng
fuente