NSRange to Range <String.Index>

258

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

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

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

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

введите описание изображения здесь

Янош
источник
71
Спасибо, Apple, за предоставление нам нового класса Range, но не за обновление ни одного из строковых служебных классов, таких как NSRegularExpression, чтобы использовать их!
puzzl

Ответы:

269

NSStringВерсия (в отличие от Swift струны) 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)
}
Алекс Прецлав
источник
14
Это не будет работать, если textFieldсодержит юникод-символы с несколькими кодами, такие как эмодзи. Поскольку это поле ввода, пользователь вполне может ввести смайлики (😂), другие символы страницы 1, состоящие из символов нескольких кодовых единиц, такие как флаги (🇪🇸).
Zaph
2
@Zaph: так как диапазон берется из UITextField, который написан в Obj-C для NSString, я подозреваю, что только допустимые диапазоны, основанные на символах юникода в строке, будут предоставлены обратным вызовом делегата, и поэтому это безопасно использовать , но я лично не проверял это.
Алекс Прецлав
2
@Zaph, это правда, моя точка зрения в том , что UITextFieldDelegate«s textField:shouldChangeCharactersInRange:replacementString:метод не обеспечивает диапазон , который начинается или заканчивается внутри юникода характер, так как обратный вызов на основе ввода пользователем символов с клавиатуры, и это невозможно напечатать только часть эмодзи юникод персонажа. Обратите внимание, что если бы делегат был написан на Obj-C, у него возникла бы та же проблема.
Алекс Прецлав
4
Не самый лучший ответ. Легче всего читать код, но у @ martin-r правильный ответ.
Рог
3
На самом деле вы, ребята, должны это сделать(textField.text as NSString?)?.stringByReplacingCharactersInRange(range, withString: string)
Wanbok Choi
361

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

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

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

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>правильно ( в том числе всех случаев Emojis, региональные индикаторы или других расширенных кластеров графемов) без промежуточного преобразования в 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
    }
}

Этот метод возвращает необязательный диапазон строк, потому что не все NSRanges действительны для данной строки 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 = "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)) // 🇩🇪

Обновление для 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 = "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)) // 🇩🇪
Мартин Р
источник
7
Этот код не работает правильно для символов, состоящих из более чем одной кодовой точки UTF-16, например, Emojis. - Например, если текстовое поле содержит текст «👿», и вы удаляете этот символ, значит range, (0,2)потому что NSRangeссылается на символы UTF-16 в NSString. Но он считается одним символом Юникода в строке Swift. - let end = advance(start, range.length)из указанного ответа происходит сбой в этом случае с сообщением об ошибке «Неустранимая ошибка: невозможно увеличить endIndex».
Мартин Р
8
@MattDiPasquale: Конечно. Мое намерение состояло в том, чтобы ответить на дословный вопрос «Как я могу преобразовать NSRange в Range <String.Index> в Swift» безопасным для Юникода способом (надеясь, что кто-то может найти его полезным, чего не происходит до сих пор :(
Мартин R
5
Еще раз спасибо за то, что сделали для них работу Apple. Без таких людей, как вы и Stack Overflow, я бы не стал заниматься разработкой для iOS / OS X. Жизнь слишком коротка.
Womble
1
@ jiminybob99: Ctrl + клик, Stringчтобы перейти к справочнику API, прочитать все методы и комментарии, а затем попробовать разные вещи, пока он не заработает :)
Martin R
1
Для let from16 = utf16.index(utf16.startIndex, offsetBy: nsRange.location, limitedBy: utf16.endIndex), я думаю , что вы на самом деле хотите , чтобы ограничить его utf16.endIndex - 1. В противном случае вы можете начать с конца строки.
Майкл Цай
18

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

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

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
источник
18

Этот ответ Мартина Р, кажется, правильный, потому что он учитывает Юникод.

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

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

Свифт 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)
    }
}
Сергей Яковенко
источник
7

Это похоже на ответ EMILIE в однако , так как вы специально просили , как преобразовать 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 байтов, а (ЛИЦО С ОЗНАКОМ РАДОСТИ) - 4 байта. (В этом конкретном случае оказывается, что количество байтов одинаково для представлений UTF-8, UTF-16 и UTF-32.)

Обернуть это в функцию:

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
zaph
источник
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)
Brenden
источник
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)
Eonil
источник
В Swift 2, return NSRange(location: a.utf16Count, length: b.utf16Count)должно быть изменено наreturn NSRange(location: a.utf16.count, length: b.utf16.count)
Elliot
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
}
Дэнни Браво
источник
Я заметил, что мой код больше не работает после последнего обновления Swift2. Я обновил свой ответ для работы с Swift2.
Дэнни Браво
0

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

user6511559
источник
Будет полезно, если вы предоставите ссылки в своем ответе.
m5khan
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
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)
    }
}
чжэн
источник