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)
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:
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)
}
func textField(textField: UITextField,
shouldChangeCharactersInRange range: NSRange,
replacementString string: String) -> Bool {
let nsString = textField.text as NSString?
let newString = nsString?.stringByReplacingCharactersInRange(range, withString: string)
}
À 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 NSRange
s 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)) // ????????
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)
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:
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
}
}
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
}
}
extension String {
func rangeFromNSRange(nsRange : NSRange) -> Range<String.Index>? {
return Range(nsRange, in: self)
}
}
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)
...
}
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
func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool {
let strString = ((textField.text)! as NSString).stringByReplacingCharactersInRange(range, withString: string)
}
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)
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
}
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)
}
}
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.
????
????????
????????
????????
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.Index
est décalage unité de code UTF-16, alors le problème devient simple. Mais il est peu probable que cela se produise.
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)
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
}
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