NSRange to Range <String.Index>

258

Wie kann ich konvertieren NSRangezu Range<String.Index>in Swift?

Ich möchte die folgende UITextFieldDelegateMethode verwenden:

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

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

Geben Sie hier die Bildbeschreibung ein

János
quelle
71
Vielen Dank, Apple, dass Sie uns die neue Range-Klasse zur Verfügung gestellt haben, aber dann keine der String-Dienstprogrammklassen wie NSRegularExpression aktualisiert haben, um sie zu verwenden!
Puzzl

Antworten:

269

Die NSStringVersion (im Gegensatz zu Swift String) von replacingCharacters(in: NSRange, with: NSString)einem übernimmt NSRange, so eine einfache Lösung ist , zu konvertieren , Stringum NSStringersten . Die Namen der Delegaten- und Ersetzungsmethoden unterscheiden sich in Swift 3 und 2 geringfügig. Je nachdem, welchen Swift Sie verwenden:

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
quelle
14
Dies funktioniert nicht, wenn textFieldUnicode-Zeichen mit mehreren Codeeinheiten wie Emoji enthalten sind. Da es sich um ein Eingabefeld handelt, kann ein Benutzer durchaus Emoji (😂) eingeben, andere Zeichen auf Seite 1 von Zeichen mit mehreren Codeeinheiten wie Flags (🇪🇸).
Zaph
2
@Zaph: Da der Bereich aus dem UITextField stammt, das in Obj-C gegen NSString geschrieben ist, vermute ich, dass nur gültige Bereiche, die auf den Unicode-Zeichen in der Zeichenfolge basieren, vom Delegaten-Rückruf bereitgestellt werden, und dies ist daher sicher zu verwenden , aber ich habe es nicht persönlich getestet.
Alex Pretzlav
2
@Zaph, das stimmt, mein Punkt ist, dass UITextFieldDelegatedie textField:shouldChangeCharactersInRange:replacementString:Methode keinen Bereich bereitstellt, der innerhalb eines Unicode-Zeichens beginnt oder endet, da der Rückruf darauf basiert, dass ein Benutzer Zeichen über die Tastatur eingibt und nicht nur ein Teil eingegeben werden kann eines Emoji-Unicode-Zeichens. Beachten Sie, dass der Delegat dasselbe Problem haben würde, wenn er in Obj-C geschrieben wäre.
Alex Pretzlav
4
Nicht die beste Antwort. Am einfachsten zu lesen, aber @ martin-r hat die richtige Antwort.
Rog
3
Eigentlich (textField.text as NSString?)?.stringByReplacingCharactersInRange(range, withString: string)
solltet
361

Ab Swift 4 (Xcode 9) bietet die Swift-Standardbibliothek Methoden zum Konvertieren zwischen Swift-Zeichenfolgenbereichen ( Range<String.Index>) und NSStringBereichen ( NSRange). Beispiel:

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

Daher kann die Textersetzung in der Textfelddelegatmethode jetzt wie folgt erfolgen

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

(Ältere Antworten für Swift 3 und früher :)

Ab Swift 1.2 String.Indexgibt es einen Initialisierer

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

die NSRangezur Range<String.Index>korrekten Konvertierung in (einschließlich aller Fälle von Emojis, Regionalindikatoren oder anderen erweiterten Graphemclustern) ohne Zwischenkonvertierung in Folgendes verwendet werden kann 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
    }
}

Diese Methode gibt einen optionalen Zeichenfolgenbereich zurück, da nicht alle NSRanges für eine bestimmte Swift-Zeichenfolge gültig sind.

Die UITextFieldDelegateDelegate-Methode kann dann als geschrieben werden

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
}

Die inverse Umwandlung ist

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

Ein einfacher Test:

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

Update für Swift 2:

Die Swift 2-Version von rangeFromNSRange()wurde bereits von Serhii Yakovenko in dieser Antwort angegeben. Der Vollständigkeit halber füge ich sie hier ein:

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

Die Swift 2 Version von NSRangeFromRange()ist

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

Update für 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
    }
}

Beispiel:

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.
quelle
7
Dieser Code funktioniert nicht richtig für Zeichen, die aus mehr als einem UTF-16-Codepunkt bestehen, z. B. Emojis. - Als Beispiel, wenn das Textfeld den Text „👿“ enthält und Sie diese Zeichen löschen, dann rangeist (0,2)da NSRangebezieht sich auf die UTF-16 - Zeichen in ein NSString. Es zählt jedoch als ein Unicode-Zeichen in einer Swift-Zeichenfolge. - let end = advance(start, range.length)Aus der referenzierten Antwort stürzt in diesem Fall mit der Fehlermeldung "Schwerwiegender Fehler: EndIndex kann nicht inkrementiert werden" ab.
Martin R
8
@ MattDiPasquale: Sicher. Meine Absicht war es, die wörtliche Frage "Wie kann ich NSRange in Swift in Range <String.Index> konvertieren?" Unicode-sicher zu beantworten (in der Hoffnung, dass jemand sie nützlich findet, was bis jetzt nicht der Fall ist :(
Martin R
5
Nochmals vielen Dank, dass Sie Apples Arbeit für sie geleistet haben. Ohne Leute wie Sie und Stack Overflow würde ich auf keinen Fall iOS / OS X entwickeln. Das Leben ist zu kurz.
Womble
1
@ jiminybob99: Klicken Sie bei gedrückter Befehlstaste auf String, um zur API-Referenz zu springen, alle Methoden und Kommentare zu lesen und dann verschiedene Dinge auszuprobieren, bis es funktioniert :)
Martin R
1
Denn let from16 = utf16.index(utf16.startIndex, offsetBy: nsRange.location, limitedBy: utf16.endIndex)ich denke, Sie wollen es tatsächlich einschränken durch utf16.endIndex - 1. Andernfalls können Sie das Ende der Zeichenfolge beginnen.
Michael Tsai
18

Sie müssen Range<String.Index>anstelle des Klassikers verwenden NSRange. Die Art und Weise, wie ich es mache (vielleicht gibt es einen besseren Weg), besteht darin, die Saite zu nehmen und sie zu String.Indexbewegen advance.

Ich weiß nicht, welchen Bereich Sie ersetzen möchten, aber tun wir so, als wollten Sie die ersten beiden Zeichen ersetzen.

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
quelle
18

Diese Antwort von Martin R scheint richtig zu sein, da sie Unicode ausmacht.

Zum Zeitpunkt des Posts (Swift 1) wird sein Code jedoch nicht in Swift 2.0 (Xcode 7) kompiliert, da die advance()Funktion entfernt wurde . Die aktualisierte Version ist unten:

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

Dies ist vergleichbar mit Emilie Antwort jedoch , da Sie speziell gefragt , wie die zu konvertieren , NSRangeum Range<String.Index>Sie so etwas wie dies tun würden:

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
quelle
2
Die Zeichenansicht und die UTF16-Ansicht einer Zeichenfolge können unterschiedliche Längen haben. Diese Funktion verwendet UTF16-Indizes (das NSRangespricht, obwohl ihre Komponenten Ganzzahlen sind) für die Zeichenansicht der Zeichenfolge, die möglicherweise fehlschlägt, wenn sie für eine Zeichenfolge mit "Zeichen" verwendet wird, für deren Ausdruck mehr als eine UTF16-Einheit erforderlich ist.
Nate Cook
6

Ein Riff über die großartige Antwort von @Emilie, keine Ersatz- / Konkurrenzantwort.
(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)")

Ausgabe:

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

Beachten Sie, dass die Indizes mehrere Codeeinheiten berücksichtigen. Das Flag (REGIONAL INDICATOR SYMBOL LETTERS ES) beträgt 8 Bytes und das (FACE WITH TEARS OF JOY) 4 Bytes. (In diesem speziellen Fall stellt sich heraus, dass die Anzahl der Bytes für UTF-8-, UTF-16- und UTF-32-Darstellungen gleich ist.)

Wickeln Sie es in eine Funktion:

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

Ausgabe:

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

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

 }
Darshan Panchal
quelle
2

In Swift 2.0 unter der Annahme 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
quelle
1

Hier ist meine größte Anstrengung. Dies kann jedoch keine falschen Eingabeargumente überprüfen oder erkennen.

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.

Ergebnis.

😂
🇪🇸
🇪🇸
🇪🇸

Einzelheiten

NSRangevon NSStringZählungen UTF-16 Code-Einheit s. Und Range<String.Index>von Swift Stringist ein undurchsichtiger relativer Typ, der nur Gleichheits- und Navigationsoperationen bietet. Dies ist absichtlich verstecktes Design.

Obwohl die Range<String.Index>scheinbar auf den UTF-16-Code-Unit-Offset abgebildet sind, ist dies nur ein Implementierungsdetail, und ich konnte keine Erwähnung für eine Garantie finden. Das heißt, die Implementierungsdetails können jederzeit geändert werden. Die interne Darstellung von Swift Stringist nicht genau definiert, und ich kann mich nicht darauf verlassen.

NSRangeWerte können direkt String.UTF16ViewIndizes zugeordnet werden. Aber es gibt keine Methode, um es umzuwandelnString.Index .

Swift String.Indexist ein Index zum Iterieren von Swift Character, einem Unicode-Graphemcluster . Dann müssen Sie NSRangedas richtige angeben, das die richtigen Graphemcluster auswählt. Wenn Sie wie im obigen Beispiel einen falschen Bereich angeben, wird ein falsches Ergebnis erzielt, da der richtige Graphemclusterbereich nicht ermittelt werden konnte.

Wenn es eine Garantie dafür gibt, dass String.Index es sich um einen UTF-16-Code-Unit-Offset handelt, wird das Problem einfach. Aber es ist unwahrscheinlich, dass es passiert.

Inverse Umwandlung

Auf jeden Fall kann die inverse Umwandlung genau durchgeführt werden.

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

Ergebnis.

(0,6)
(4,2)
Eonil
quelle
In Swift 2 return NSRange(location: a.utf16Count, length: b.utf16Count)muss zureturn NSRange(location: a.utf16.count, length: b.utf16.count)
Elliot
1

Ich habe festgestellt, dass die sauberste Lösung von swift2 nur darin besteht, eine Kategorie in NSRange zu erstellen:

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

Rufen Sie es dann für die Textfelddelegatfunktion auf:

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
quelle
Ich habe festgestellt, dass mein Code nach dem letzten Swift2-Update nicht mehr funktioniert. Ich habe meine Antwort aktualisiert, um mit Swift2 zu arbeiten.
Danny Bravo
0

Die offizielle Beta-Dokumentation zu Swift 3.0 bietet die Standardlösung für diese Situation unter dem Titel String.UTF16View im Abschnitt UTF16View Elements Match NSString Characters title

user6511559
quelle
Es ist hilfreich, wenn Sie in Ihrer Antwort Links angeben.
m5khan
0

In der akzeptierten Antwort finde ich die Optionen umständlich. Dies funktioniert mit Swift 3 und scheint kein Problem mit Emojis zu haben.

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