web-dev-qa-db-fra.com

NSRange to Range <String.Index>

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

Je souhaite utiliser la méthode UITextFieldDelegate suivante:

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

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

enter image description here

219
János

La version NSString (par opposition à Swift String) de replacingCharacters(in: NSRange, with: NSString) accepte une NSRange; une solution simple consiste donc à convertir String en NSString en premier . 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 type de 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)
}
253
Alex Pretzlav

À partir de Swift 4 (Xcode 9), la bibliothèque standard De Swift fournit des méthodes de conversion entre les plages de chaînes Swift (Range<String.Index>) et les plages NSString (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égation de champ de texte .__ peut maintenant être effectué en tant que

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

À partir de Swift 1.2, String.Index a un initialiseur

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

qui peut être utilisé pour convertir NSRange en Range<String.Index> correctement (y compris tous les cas d’Emojis, d’indicateurs régionaux ou d’autres clusters étendus grapheme) sans conversion intermédiaire en 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 facultatif parce que tous les NSRanges Ne sont pas valides pour une chaîne Swift donnée.

La méthode de délégué UITextFieldDelegate peut ensuite être écrite en tant que

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’inclusici pour plus de renseignements:

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() 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(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)) // ????????
289
Martin R

Vous devez utiliser Range<String.Index> au lieu du classique NSRange. La façon dont je le fais (peut-être y a-t-il un meilleur moyen) consiste-t-il à utiliser le String.Index de la chaîne pour le déplacer avec advance.

Je ne sais pas quelle plage vous essayez de remplacer, mais supposons que vous souhaitiez 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)
18
Emilie

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 compile pas dans Swift 2.0 (Xcode 7), car ils ont supprimé la fonction advance(). 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)
    }
}
17
Serhii Yakovenko

Ceci est similaire à la réponse d'Emilie. Cependant, puisque vous avez demandé spécifiquement comment convertir la NSRange en Range<String.Index>, vous feriez 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) 
     ...

}
7
James Jones

Un riff sur l'excellente réponse de @Emilie, pas une réponse de remplacement/concurrent.
(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)")

Sortie: 

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 (LETTRES ES SYMBOLE REGIONAL DE L’INDICATEUR) est de 8 octets et celui de (visage avec des larmes de joie) 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 un 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)")

Sortie: 

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

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

 }
4
Darshan Panchal

Dans Swift 2.0 en supposant que 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)
2
Brenden

J'ai trouvé que la seule solution propre à Swift2 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 appelez-le à partir de 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
}
1
Danny Bravo
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)
    }
}
1
Z King

Voici mon meilleur effort. Mais cela ne peut pas vérifier ou détecter un argument de saisie 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 de NSString compte UTF-16 unité-de code s. Et Range<String.Index> de Swift String est un type relatif opaque qui fournit uniquement des opérations d'égalité et de navigation. C'est une conception intentionnellement cachée.

Bien que le Range<String.Index> semble être mappé sur le décalage d'unité de code UTF-16, il ne s'agit que d'un détail d'implémentation, et je n'ai trouvé aucune mention concernant une garantie. Cela signifie que les détails de la mise en œuvre peuvent être modifiés à tout moment. La représentation interne de Swift String n'est pas bien définie et je ne peux pas compter dessus.

Les valeurs NSRange peuvent être directement mappées sur les index String.UTF16View. Mais il n'y a pas de méthode pour le convertir en String.Index.

Swift String.Index est un index pour itérer Swift Character qui est un cluster de graphèmes Unicode. Ensuite, vous devez fournir la variable NSRange appropriée qui sélectionne les grappes de graphèmes correctes. Si vous indiquez une plage incorrecte, comme dans l'exemple ci-dessus, le résultat sera erroné car la plage de grappes de graphèmes appropriée ne peut pas être déterminée. 

S'il existe une garantie que le String.Indexest décalage unité de code UTF-16, alors le 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)
1
Eonil

Dans la réponse acceptée, les options sont lourdes. Cela fonctionne avec Swift 3 et ne semble pas avoir de problème avec les émoticônes. 

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
Murray Sagal

La documentation officielle de Swift 3.0 beta fournit sa solution standard pour cette situation sous le titre String.UTF16View dans la section UTF16View Elements Match NSString Characters title

0
user6511559