NSRange to Range <String.Index>

258

Jak mogę przekonwertować NSRangena Range<String.Index>Swift?

Chcę użyć następującej UITextFieldDelegatemetody:

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

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

wprowadź opis zdjęcia tutaj

János
źródło
71
Dziękujemy, Apple, że dałeś nam nową klasę Range, ale nie zaktualizowałeś żadnej z klas narzędzi łańcuchowych, takich jak NSRegularExpression, aby ich użyć!
puzzl

Odpowiedzi:

269

NSStringWersja (w przeciwieństwie do Swift łańcucha znaków) replacingCharacters(in: NSRange, with: NSString)akceptuje NSRange, aby jedno proste rozwiązanie polega na konwersji Stringdo NSStringpierwszej . Nazwy metod delegowania i wymiany są nieco inne w Swift 3 i 2, więc w zależności od używanego Swift:

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
źródło
14
To nie zadziała, jeśli textFieldzawiera znaki Unicode z wieloma kodami, takie jak emoji. Ponieważ jest to pole wejściowe, użytkownik może równie dobrze wprowadzić emoji (😂), inne znaki na stronie 1 znaków wielu jednostek kodu, takie jak flagi (🇪🇸).
zaph
2
@Zaph: ponieważ zakres pochodzi z UITextField, który jest napisany w Obj-C przeciwko NSString, podejrzewam, że tylko prawidłowe zakresy oparte na znakach Unicode w łańcuchu byłyby dostarczane przez wywołanie zwrotne delegata, więc można z niego bezpiecznie korzystać , ale osobiście tego nie testowałem.
Alex Pretzlav
2
@Zaph, to prawda, chodzi mi o to, że UITextFieldDelegatejest textField:shouldChangeCharactersInRange:replacementString:metoda nie zapewni szereg która rozpoczyna się lub kończy wewnątrz Unicode, gdyż zwrotna jest oparty na wprowadzanie znaków z klawiatury przez użytkownika, a to nie jest możliwe, aby wpisać tylko część znaku Unicode emoji. Zauważ, że gdyby delegat został napisany w Obj-C, miałby ten sam problem.
Alex Pretzlav
4
Nie najlepsza odpowiedź. Najłatwiejszy do odczytania kod, ale @ martin-r ma poprawną odpowiedź.
Rog
3
Właściwie powinniście to zrobić(textField.text as NSString?)?.stringByReplacingCharactersInRange(range, withString: string)
Wanbok Choi,
361

Począwszy od Swift 4 (Xcode 9), standardowa biblioteka Swift zapewnia metody konwersji między zakresami ciągów Swift ( Range<String.Index>) i NSStringzakresami ( NSRange). Przykład:

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

Dlatego zamiana tekstu w metodzie delegowania pola tekstowego może być teraz wykonywana jako

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

(Starsze odpowiedzi dla Swift 3 i wcześniejszych :)

Począwszy od wersji Swift 1.2, String.Indexma inicjalizator

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

który może być stosowany do konwersji NSRangedo Range<String.Index>prawidłowego (w tym wszystkich przypadkach emotikony, wskaźniki regionalnych lub innych wydłużonych klastrów grafem) bez pośredniego konwersję do 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
    }
}

Ta metoda zwraca opcjonalny zakres ciągów, ponieważ nie wszystkie NSRanges są poprawne dla danego ciągu Swift.

UITextFieldDelegateMetoda delegata można następnie zapisać jako

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
}

Odwrotna konwersja to

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

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

Aktualizacja dla Swift 2:

Wersja Swift 2 rangeFromNSRange()została już podana przez Serhii Yakovenko w tej odpowiedzi , zamieszczam ją tutaj dla kompletności:

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

Wersja Swift 2 NSRangeFromRange()to

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

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

Przykład:

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.
źródło
7
Ten kod nie działa poprawnie dla znaków składających się z więcej niż jednego punktu kodowego UTF-16, np. Emoji. - Na przykład, jeśli pole tekstowe zawiera tekst „👿”, a ty usuniesz ten znak, rangeoznacza to, (0,2)że NSRangeodnosi się do znaków UTF-16 w NSString. Ale liczy się jako jeden znak Unicode w ciągu Swift. - let end = advance(start, range.length)z przywołanej odpowiedzi ulega awarii w tym przypadku z komunikatem o błędzie „błąd krytyczny: nie można zwiększyć parametru endIndex”.
Martin R
8
@MattDiPasquale: Pewnie. Moim zamiarem było odpowiedzenie na dosłowne pytanie „Jak przekonwertować NSRange na Range <String.Index> w Swift” w sposób bezpieczny dla Unicode (mając nadzieję, że ktoś może uznać to za przydatne, co do tej pory tak nie było :(
Martin R
5
Jeszcze raz dziękuję za wykonanie dla nich pracy Apple. Bez ludzi takich jak ty i stosu przepełnienia stosu nie ma mowy, bym rozwijał system iOS / OS X. Życie jest zbyt krótkie.
Womble,
1
@ jiminybob99: Kliknij klawisz Command, Stringaby przejść do odwołania do interfejsu API, przeczytaj wszystkie metody i komentarze, a następnie wypróbuj różne rzeczy, aż zadziała :)
Martin R
1
Za let from16 = utf16.index(utf16.startIndex, offsetBy: nsRange.location, limitedBy: utf16.endIndex), myślę, że rzeczywiście chcemy ograniczać go utf16.endIndex - 1. W przeciwnym razie możesz zacząć od końca łańcucha.
Michael Tsai,
18

Musisz użyć Range<String.Index>zamiast klasycznego NSRange. Sposób, w jaki to robię (być może jest lepszy sposób), polega na tym, że ciąg jest String.Indexruchomy advance.

Nie wiem, jaki zakres próbujesz zastąpić, ale udawaj, że chcesz zamienić pierwsze 2 znaki.

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
źródło
18

Ta odpowiedź Martina R wydaje się poprawna, ponieważ uwzględnia Unicode.

Jednak w momencie publikacji (Swift 1) jego kod nie kompiluje się w Swift 2.0 (Xcode 7), ponieważ usunęły one advance()funkcję. Zaktualizowana wersja znajduje się poniżej:

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

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

Szybki 4

extension String {
    func rangeFromNSRange(nsRange : NSRange) -> Range<String.Index>? {
        return Range(nsRange, in: self)
    }
}
Serhii Yakovenko
źródło
7

Jest to podobne do odpowiedzi Emilie jednak skoro pytasz konkretnie w jaki sposób przekonwertować NSRangedo Range<String.Index>ciebie by zrobić coś takiego:

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
źródło
2
Widok znaków i widok UTF16 łańcucha mogą mieć różne długości. Ta funkcja korzysta z indeksów UTF16 (to NSRangemówi, chociaż jej komponenty są liczbami całkowitymi) w widoku znaków ciągu, co może się nie powieść, gdy zostanie użyte w ciągu znaków z „znakami”, których wyrażenie wymaga więcej niż jednej jednostki UTF16.
Nate Cook
6

Riff na świetnej odpowiedzi @ Emilie, a nie odpowiedź zastępcza / konkurencyjna.
(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)")

Wynik:

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

Zwróć uwagę, że indeksy uwzględniają wiele jednostek kodu. Flaga (REGIONAL INDICATOR SYMBOL LETTERS ES) ma 8 bajtów, a (TWARZ Z ŁZY RADOŚCI) ma 4 bajty. (W tym konkretnym przypadku okazuje się, że liczba bajtów jest taka sama dla reprezentacji UTF-8, UTF-16 i UTF-32).

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

Wynik:

newString: !his is a test
zaph
źródło
4
func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool {

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

 }
Darszan Panchal
źródło
2

W Swift 2.0 przy założeniu 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
źródło
1

Oto mój najlepszy wysiłek. Ale to nie może sprawdzić lub wykryć nieprawidłowego argumentu wejściowego.

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.

Wynik.

😂
🇪🇸
🇪🇸
🇪🇸

Detale

NSRangez NSStringzlicza kod-jednostka UTF-16 . A Range<String.Index>od Swift Stringjest nieprzezroczysty typ względny, który zapewnia tylko operacje równości i nawigacji. Jest to celowo ukryty projekt.

Chociaż Range<String.Index>wydaje się, że jest odwzorowany na przesunięcie jednostki kodu UTF-16, jest to tylko szczegół implementacji i nie mogłem znaleźć żadnej wzmianki o żadnej gwarancji. Oznacza to, że szczegóły implementacji można zmienić w dowolnym momencie. Wewnętrzna reprezentacja Swift Stringnie jest dość zdefiniowana i nie mogę na niej polegać.

NSRangewartości mogą być bezpośrednio mapowane na String.UTF16Viewindeksy. Ale nie ma metody na konwersję String.Index.

Swift String.Indexjest indeksem do iteracji Swift, Characterktóra jest klastrem grafodowym w Unicode . Następnie musisz podać właściwy, NSRangektóry wybiera prawidłowe klastry grafemów. Jeśli podasz zły zakres, jak w powyższym przykładzie, spowoduje to niepoprawny wynik, ponieważ nie można ustalić właściwego zakresu skupień grafemu.

Jeśli istnieje gwarancja, że String.Index jest to przesunięcie jednostki kodu UTF-16, problem staje się prosty. Ale jest mało prawdopodobne.

Odwrotna konwersja

W każdym razie odwrotną konwersję można wykonać dokładnie.

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

Wynik.

(0,6)
(4,2)
Eonil
źródło
W Swift 2 return NSRange(location: a.utf16Count, length: b.utf16Count)należy zmienić nareturn NSRange(location: a.utf16.count, length: b.utf16.count)
Elliot
1

Znalazłem najczystsze rozwiązanie tylko dla swift2, aby utworzyć kategorię na 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)
    }
}

A następnie wywołaj go z funkcji delegowania pola tekstowego:

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
źródło
Zauważyłem, że mój kod przestał działać po ostatniej aktualizacji Swift2. Zaktualizowałem swoją odpowiedź do pracy ze Swift2.
Danny Bravo
0

Oficjalna dokumentacja Swift 3.0 beta podała standardowe rozwiązanie tej sytuacji pod tytułem String.UTF16View w sekcji UTF16View Elements Match NSString Postacie

użytkownik6511559
źródło
Pomocne będzie podanie linków w odpowiedzi.
m5khan
0

W przyjętej odpowiedzi uważam opcje za niewygodne. Działa to z Swift 3 i wydaje się, że nie ma problemu z emoji.

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
źródło
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
źródło