NSRange to Range <String.Index>

171

Как преобразовать NSRange в Range<String.Index> в Swift?

Я хочу использовать следующий метод UITextFieldDelegate:

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

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

Изображение 65

  • 62
    Спасибо, Apple, за предоставление нам нового класса Range, но не за обновление ни одного из строковых служебных классов, таких как NSRegularExpression, чтобы использовать их!
  • 0
    stackoverflow.com/a/43233619/2303865
Теги:
ios8
nsstring
nsrange

12 ответов

240
Лучший ответ

Версия NSString (в отличие от Swift String) replacingCharacters(in: NSRange, with: NSString) принимает NSRange, поэтому одно простое решение - преобразовать String в NSString сначала. Названия делегатов и методов замены немного отличаются в Swift 3 и 2, поэтому в зависимости от того, какой 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)
}
  • 2
    @Zaph: так как диапазон берется из UITextField, который написан в Obj-C для NSString, я подозреваю, что только допустимые диапазоны, основанные на символах юникода в строке, будут предоставлены обратным вызовом делегата, и поэтому это безопасно использовать , но я лично не проверял это.
  • 0
    Юникод, все это, поддерживается NSString и, учитывая UITextField пользователь вполне может ввести многозначный символ Unicode UTF-16. NSString основывается на UTF-16 и работает с блоками UTF-16 и возвращает количество блоков UTF-16, а не символов. Таким образом, NSRange может начинаться или заканчиваться внутри символа юникода, такого как эмодзи. В плоскости 0 также есть суррогатные пары Юникода.
Показать ещё 7 комментариев
196

Как и у Swift 4 (Xcode 9), стандарт Swift библиотека предоставляет методы для преобразования строк строки Swift (Range<String.Index>) и NSString (NSRange). Пример:

let str = "abc"
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: text)!
print(str.substring(with: r2)) // 

Поэтому замена текста в методе делегирования текстового поля теперь можно сделать как

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

(Старые ответы для Swift 3 и ранее:)

Как и в Swift 1.2, String.Index имеет инициализатор

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

который можно использовать для правильного преобразования NSRange в Range<String.Index> (включая все случаи Эмоджи, Региональные индикаторы или другие расширенные grapheme clusters) без промежуточного преобразования в 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
    }
}

Этот метод возвращает необязательный диапазон строк, поскольку не все NSRange s действительны для данной строки Swift.

Метод делегата UITextFieldDelegate может быть записан как

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
}

Обратное преобразование

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

Простой тест:

let str = "abc"
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)) // 

Обновление для Swift 2:

Версия Swift 2 rangeFromNSRange() уже указана Сергей Яковенко в этот ответ, я включаю его здесь для полноты:

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 2 NSRangeFromRange() равна

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

Обновление для 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
    }
}

Пример:

let str = "abc"
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)) // 
  • 0
    Почему бы просто не сделать stackoverflow.com/a/26749314/242933 ? Вроде меньше кода и все равно правильно.
  • 0
    Хмм хорошо. Благодарю. Я думаю, что тогда я просто конвертирую String в NSString, так как это, кажется, работает и требует меньше кода (проще).
Показать ещё 20 комментариев
16

Вам нужно использовать Range<String.Index> вместо классического NSRange. То, как я это делаю (может быть, есть лучший способ), - это взять строку String.Index, перемещая ее с помощью advance.

Я не знаю, какой диапазон вы пытаетесь заменить, но пусть притворяется, что вы хотите заменить первые 2 символа.

var start = textField.text.startIndex // Start at the string 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)
11

Этот ответ от Martin R кажется правильным, потому что он учитывает Unicode.

Однако в настоящее время его код не компилируется в Swift 2.0 (Xcode 7), поскольку они удалили функцию advance(). Обновленная версия:

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

Это похоже на ответ Эмилии, однако, поскольку вы спросили, как преобразовать NSRange в Range<String.Index>, вы сделали бы что-то вроде этого:

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

}
  • 2
    Вид символов и вид строки UTF16 могут иметь разную длину. Эта функция использует индексы UTF16 (это то, что говорит NSRange , хотя ее компоненты являются целыми числами) в представлении символов строки, что может привести к сбою при использовании строки со «символами», для выражения которой требуется более одного модуля UTF16.
6

Рифф на отличный ответ от @Emilie, а не на альтернативный/конкурирующий ответ.
(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)")

Выход:

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

Обратите внимание на учетную запись индексов для нескольких единиц кода. Флаг (РЕГИОНАЛЬНЫЕ ИНДИКАТОРЫ СИМВОЛЫ ПИСЬМА ES) составляет 8 байтов, а (FACE WITH TEARS OF JOY) - 4 байта. (В этом конкретном случае оказывается, что количество байтов одинаково для представлений UTF-8, UTF-16 и UTF-32.)

Обертка в 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)")

Выход:

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

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

 }
2

В Swift 2.0 при условии 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)
1

Я нашел, что самое чистое решение swift2 только для создания категории на 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)
    }
}

И затем вызовите его для функции делегата текстового поля:

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
}
  • 0
    Я заметил, что мой код больше не работает после последнего обновления Swift2. Я обновил свой ответ для работы с Swift2.
1

Вот мои лучшие усилия. Но это не может проверить или обнаружить неправильный входной аргумент.

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.

Результат.





Подробнее

NSRange от NSString подсчитывает единицы кода UTF-16. И Range<String.Index> из Swift String - непрозрачный относительный тип, который обеспечивает только операции равенства и навигации. Это намеренно скрытый дизайн.

Несмотря на то, что Range<String.Index>, по-видимому, отображается на смещение кода кода UTF-16, это всего лишь деталь реализации, и я не мог найти упоминания о какой-либо гарантии. Это означает, что детали реализации могут быть изменены в любое время. Внутреннее представление Swift String не совсем определено, и я не могу положиться на него.

Значения

NSRange могут быть непосредственно сопоставлены с индексами String.UTF16View. Но нет способа преобразовать его в String.Index.

Swift String.Index - это индекс для итерации Swift Character, который является кластером графем Unicode. Затем вы должны предоставить правильный NSRange, который выбирает правильные кластеры графемы. Если вы указали неправильный диапазон, как в приведенном выше примере, это приведет к неправильному результату, потому что нельзя было определить правильный диапазон кластеров графема.

Если есть гарантия, что String.Index является сдвигом кода кода UTF-16, проблема становится простой. Но это вряд ли произойдет.

Обратное преобразование

В любом случае обратное преобразование может быть выполнено точно.

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

Результат.

(0,6)
(4,2)
  • 0
    В Swift 2 необходимо return NSRange(location: a.utf16Count, length: b.utf16Count) чтобы оно return NSRange(location: a.utf16.count, length: b.utf16.count)
0

В принятом ответе я нахожу, что варианты громоздки. Это работает с Swift 3 и, похоже, не имеет проблем с 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
}
0

Официальная документация по Swift 3.0 beta предоставила стандартное решение для этой ситуации под заголовком String.UTF16View в разделе UTF16View Элементы Match NSString Название персонажа

Ещё вопросы

Сообщество Overcoder
Наверх
Меню