NSRange to Range <String.Index>

258

Comment puis - je convertir NSRangeà Range<String.Index>Swift?

Je souhaite utiliser la UITextFieldDelegateméthode suivante :

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

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

entrez la description de l'image ici

János
la source
71
Merci, Apple, de nous avoir donné la nouvelle classe Range, mais sans avoir mis à jour aucune des classes d'utilitaires de chaîne, telles que NSRegularExpression, pour les utiliser!
puzzl

Réponses:

269

La NSStringversion (par opposition à Swift String) replacingCharacters(in: NSRange, with: NSString)accepte un NSRange, donc une solution simple est de convertir Stringen NSStringpremier . Les noms des méthodes de délégué et de remplacement sont légèrement différents dans Swift 3 et 2, donc selon le Swift que vous utilisez:

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
la source
14
Cela ne fonctionnera pas s'il textFieldcontient des caractères unicode d'unité multi-code tels que des emoji. Puisqu'il s'agit d'un champ de saisie, un utilisateur peut très bien saisir des emoji (😂), d'autres caractères de la page 1 de caractères d'unité de code multiple tels que des drapeaux (🇪🇸).
zaph
2
@Zaph: puisque la plage provient de UITextField, qui est écrit en Obj-C contre NSString, je soupçonne que seules les plages valides basées sur les caractères unicode de la chaîne seraient fournies par le rappel du délégué, et donc c'est sûr à utiliser , mais je ne l'ai pas personnellement testé.
Alex Pretzlav
2
@Zaph, c'est vrai, mon point est que UITextFieldDelegatela textField:shouldChangeCharactersInRange:replacementString:méthode ne fournira pas une plage qui commence ou se termine à l'intérieur d'un caractère unicode, car le rappel est basé sur un utilisateur saisissant des caractères à partir du clavier, et il n'est pas possible de taper seulement une partie d'un caractère emoji unicode. Notez que si le délégué était écrit en Obj-C, il aurait ce même problème.
Alex Pretzlav
4
Pas la meilleure réponse. Le code est plus facile à lire, mais @ martin-r a la bonne réponse.
Rog
3
En fait, vous devriez le faire(textField.text as NSString?)?.stringByReplacingCharactersInRange(range, withString: string)
Wanbok Choi
361

Au Swift 4 (Xcode 9), la bibliothèque standard Swift fournit des méthodes de conversion entre les plages de chaînes Swift ( Range<String.Index>) et les NSStringplages ( NSRange). Exemple:

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

Par conséquent, le remplacement de texte dans la méthode de délégué de champ de texte peut maintenant être effectué comme

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

(Anciennes réponses pour Swift 3 et versions antérieures :)

Depuis Swift 1.2, String.Index possède un initialiseur

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

qui peut être utilisé pour convertir NSRangeà Range<String.Index>correctement (y compris tous les cas de emoji, des indicateurs régionaux ou d' autres groupes de graphèmes étendus) sans conversion intermédiaire à 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
    }
}

Cette méthode renvoie une plage de chaînes facultative car toutesNSRange s ne sont pas valides pour une chaîne Swift donnée.

La UITextFieldDelegateméthode déléguée peut alors être écrite comme

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 conversion inverse est

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

Mise à jour pour Swift 2:

La version Swift 2 de rangeFromNSRange()a déjà été donnée par Serhii Yakovenko dans cette réponse , je l'inclus ici pour être complet:

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 version Swift 2 de NSRangeFromRange()is

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

Mise à jour pour 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
    }
}

Exemple:

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
la source
7
Ce code ne fonctionne pas correctement pour les caractères composés de plusieurs points de code UTF-16, par exemple les Emojis. - Par exemple, si le champ de texte contient le texte "👿" et que vous supprimez ce caractère, rangec'est (0,2)parce qu'il NSRangefait référence aux caractères UTF-16 dans un NSString. Mais il compte comme un caractère Unicode dans une chaîne Swift. - à let end = advance(start, range.length)partir de la réponse référencée se bloque dans ce cas avec le message d'erreur "erreur fatale: impossible d'incrémenter endIndex".
Martin R
8
@MattDiPasquale: Bien sûr. Mon intention était de répondre à la question textuelle "Comment puis-je convertir NSRange en Range <String.Index> dans Swift" de manière sécurisée Unicode (en espérant que quelqu'un puisse le trouver utile, ce qui n'est pas le cas jusqu'à présent :(
Martin R
5
Merci encore une fois d'avoir fait le travail d'Apple pour eux. Sans des gens comme vous et Stack Overflow, je ne pourrais pas faire de développement iOS / OS X. La vie est trop courte.
Womble du
1
@ jiminybob99: Commandez-cliquez sur Stringpour accéder à la référence API, lisez toutes les méthodes et commentaires, puis essayez différentes choses jusqu'à ce que cela fonctionne :)
Martin R
1
Car let from16 = utf16.index(utf16.startIndex, offsetBy: nsRange.location, limitedBy: utf16.endIndex), je pense que vous voulez vraiment le limiter par utf16.endIndex - 1. Sinon, vous pouvez commencer la fin de la chaîne.
Michael Tsai
18

Vous devez utiliser à la Range<String.Index>place du classique NSRange. La façon dont je le fais (peut-être qu'il y a une meilleure façon) est de prendre la corde et de String.Indexla déplacer advance.

Je ne sais pas quelle plage vous essayez de remplacer, mais supposons que vous vouliez remplacer les 2 premiers caractères.

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
la source
18

Cette réponse de Martin R semble être correcte car elle représente Unicode.

Cependant au moment de la publication (Swift 1) son code ne se compile pas dans Swift 2.0 (Xcode 7), car ils ont supprimé advance() fonction. La version mise à jour est ci-dessous:

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
la source
7

Ceci est similaire à la réponse d'Emilie cependant puisque vous avez demandé spécifiquement comment convertir le NSRangeto Range<String.Index>you ferait quelque chose comme ceci:

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
la source
2
La vue des caractères et la vue UTF16 d'une chaîne peuvent avoir des longueurs différentes. Cette fonction utilise des index UTF16 (c'est ce qui NSRangeparle, bien que ses composants soient des entiers) contre la vue des caractères de la chaîne, qui peut échouer lorsqu'elle est utilisée sur une chaîne avec des "caractères" qui nécessitent plus d'une unité UTF16 pour s'exprimer.
Nate Cook
6

Un riff sur la grande réponse de @Emilie, pas une réponse de remplacement / concurrente.
(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)")

Production:

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

Notez que les index représentent plusieurs unités de code. Le drapeau (REGIONAL INDICATOR SYMBOL LETTERS ES) est de 8 octets et le (VISAGE AVEC DES LARMES DE JOIE) est de 4 octets. (Dans ce cas particulier, il s'avère que le nombre d'octets est le même pour les représentations UTF-8, UTF-16 et UTF-32.)

Envelopper dans une fonction:

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

Production:

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

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

 }
Darshan Panchal
la source
2

Dans Swift 2.0 en supposant 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
la source
1

Voici mon meilleur effort. Mais cela ne peut pas vérifier ou détecter un argument d'entrée incorrect.

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.

Résultat.

😂
🇪🇸
🇪🇸
🇪🇸

Détails

NSRangeà partir des NSStringdécomptes UTF-16 code-unité s. Et Range<String.Index>de Swift Stringest un opaque type relatif qui ne fournit que des opérations d'égalité et de navigation. C'est une conception intentionnellement cachée.

Bien que cela Range<String.Index>semble être mappé sur le décalage d'unité de code UTF-16, ce n'est qu'un détail d'implémentation, et je n'ai trouvé aucune mention de garantie. Cela signifie que les détails de mise en œuvre peuvent être modifiés à tout moment. Représentation interne de SwiftString n'est pas assez définie, et je ne peux pas m'y fier.

NSRangeles valeurs peuvent être directement mappées aux String.UTF16Viewindex. Mais il n'y a pas de méthode pour le convertir enString.Index .

Swift String.Indexest un index pour itérer Swift Characterqui est un cluster de graphèmes Unicode . Ensuite, vous devez fournir une bonne NSRangesélection des grappes de graphèmes correctes. Si vous fournissez une plage incorrecte comme dans l'exemple ci-dessus, cela produira un résultat incorrect car la plage de grappe de graphèmes appropriée n'a pas pu être déterminée.

S'il y a une garantie que le String.Index est un code-unité UTF-16 offset, puis problème devient simple. Mais il est peu probable que cela se produise.

Conversion inverse

Quoi qu'il en soit, la conversion inverse peut être effectuée avec précision.

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

Résultat.

(0,6)
(4,2)
Eonil
la source
Dans Swift 2, return NSRange(location: a.utf16Count, length: b.utf16Count)doit être changé pourreturn NSRange(location: a.utf16.count, length: b.utf16.count)
Elliot
1

J'ai trouvé que la solution swift2 la plus propre consiste à créer une catégorie sur 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)
    }
}

Et puis appelez-le depuis pour la fonction de délégué de champ de texte:

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
la source
J'ai remarqué que mon code ne fonctionnait plus après la dernière mise à jour de Swift2. J'ai mis à jour ma réponse pour travailler avec Swift2.
Danny Bravo
0

La documentation officielle de la version bêta de Swift 3.0 a fourni sa solution standard pour cette situation sous le titre String.UTF16View dans la section UTF16View Elements Match NSString Characters title

user6511559
la source
Il sera utile de fournir des liens dans votre réponse.
m5khan
0

Dans la réponse acceptée, je trouve les options lourdes. Cela fonctionne avec Swift 3 et ne semble avoir aucun problème avec les 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
la source
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
la source