nsstring swift ios8 nsrange

nsstring - NSRange to Range<String.Index>



swift ios8 (13)

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 rangos NSRange ( 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, la sustitución 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.Index tiene un inicializador

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

que se puede usar para convertir NSRange a Range<String.Index> correctamente (incluidos todos los casos de Emojis, indicadores regionales u otros clústeres de grafemas extendidos) 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 NSRange s son válidos para una cadena Swift dada.

El método delegado de UITextFieldDelegate 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 Swift 2 de rangeFromNSRange() ya fue dada por Serhii Yakovenko en esta respuesta , la estoy incluyendo 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)) // πŸ‡©πŸ‡ͺ

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

Quiero usar el siguiente método UITextFieldDelegate :

func textField(textField: UITextField!, shouldChangeCharactersInRange range: NSRange, replacementString string: String!) -> Bool { textField.text.stringByReplacingCharactersInRange(???, withString: string)


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

NSRange de NSString cuenta las NSString de código UTF-16. Y el Range<String.Index> de Swift String es un tipo relativo opaco que proporciona solo operaciones de igualdad y navegación. Esto es intencionalmente diseño oculto.

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

NSRange valores de NSRange se pueden asignar directamente a los índices String.UTF16View . Pero no hay un método para convertirlo en String.Index .

Swift String.Index es un índice para iterar el Character Swift que es un clúster de grafema Unicode . Luego, debe proporcionar un NSRange adecuado que seleccione los grupos de grafemas correctos. Si proporciona un rango incorrecto como el ejemplo anterior, producirá un resultado incorrecto porque no se pudo determinar el rango correcto del grupo de grafemas.

Si hay una garantía de que el String.Index es el desplazamiento de la unidad de código UTF-16, entonces el problema se vuelve simple. 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)


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)


En la respuesta aceptada encuentro las opciones engorrosas. Esto funciona con Swift 3 y parece que no tiene 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 }


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 llámelo desde para 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 }


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 characters


La versión NSString (a diferencia de Swift String) de la replacingCharacters(in: NSRange, with: NSString) acepta un NSRange , por lo que una solución simple es convertir primero String en NSString . 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és 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) }


Sin embargo, esto es similar a la respuesta de Emilie, ya que usted preguntó específicamente cómo convertir NSRange a Range<String.Index> 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) ... }


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 cuentan para múltiples unidades de código. La bandera (LISTA DE SÍMBOLOS DE INDICADOR REGIONAL) es de 8 bytes y la (CARA CON TIROS DE ALEGRÍA) es de 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 func:

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


Range<String.Index> usar Range<String.Index> lugar del NSRange clásico. La forma en que lo hago (tal vez hay una mejor manera) es tomando el String.Index la cadena y moviéndolo con advance .

No sé qué rango intenta 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)


Esta respuesta de Martin R parece ser correcta porque da cuenta de Unicode.

Sin embargo, en el momento de la publicación (Swift 1) su código no se compila en Swift 2.0 (Xcode 7), ya que eliminaron la función advance() . 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) } }


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


func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool { let strString = ((textField.text)! as NSString).stringByReplacingCharactersInRange(range, withString: string) }