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