NSRange для диапазона

Как преобразовать NSRange в Range в Swift?

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

  func textField(textField: UITextField!, shouldChangeCharactersInRange range: NSRange, replacementString string: String!) -> Bool { textField.text.stringByReplacingCharactersInRange(???, withString: string) 

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

Версия 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) } 

С Swift 4 (Xcode 9) стандартная библиотека Swift предоставляет методы для преобразования между строковыми диапазонами Swift ( Range ) и диапазонами NSString ( 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 преобразования NSRange в Range (включая все случаи Emojis, региональных индикаторов или других расширенных кластеров grapheme) без промежуточного преобразования в NSString :

 extension String { func rangeFromNSRange(nsRange : NSRange) -> Range? { 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 делегата 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) -> 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() уже была дана Сергеем Яковенко в этом ответе , я rangeFromNSRange() его здесь для полноты:

 extension String { func rangeFromNSRange(nsRange : NSRange) -> Range? { 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) -> 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) -> 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? { 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)) // 🇩🇪 

Вам нужно использовать Range вместо classического 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 = Range(start: start,end: end) textField.text.stringByReplacingCharactersInRange(range, withString: string) 

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

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

Swift 2

 extension String { func rangeFromNSRange(nsRange : NSRange) -> Range? { 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? { 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? { return Range(nsRange, in: self) } } 

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

 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(start: start, end: end) ... } 

Рифф на отличный ответ от @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 
 func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool { let strString = ((textField.text)! as NSString).stringByReplacingCharactersInRange(range, withString: string) } 

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

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

 extension String { /// :r: Must correctly select proper UTF-16 code-unit range. Wrong range will produce wrong result. public func convertRangeFromNSRange(r:NSRange) -> Range { 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(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 из Swift Stringнепрозрачный относительный тип, который обеспечивает только операции равенства и навигации. Это намеренно скрытый дизайн.

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

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

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

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

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

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

 extension String { /// O(1) if `self` is optimised to use UTF-16. /// O(n) otherwise. public func convertRangeToNSRange(r:Range) -> NSRange { let a = substringToIndex(r.startIndex) let b = substringWithRange(r) return NSRange(location: a.utf16Count, length: b.utf16Count) } } println(convertRangeToNSRange(s.startIndex.. 

Результат.

 (0,6) (4,2) 

Я нашел, что самое чистое решение swift2 только для создания категории на NSRange:

 extension NSRange { func stringRangeForText(string: String) -> Range { let start = string.startIndex.advancedBy(self.location) let end = start.advancedBy(self.length) return Range(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 } 

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

В принятом ответе я считаю, что варианты громоздки. Это работает с 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 } 
  • Как использовать пользовательские ключи с протоколом Swift 4 Decodable?
  • Как получить текущее время как время
  • Почему «nil» не совместим с «UnsafePointer » в Swift 3?
  • Как оценивается Swift IF LET?
  • Как создать привязанную строку с помощью Swift?
  • Массивы декодирования Swift JSONDecode терпят неудачу, если сбой одного элемента
  • Как сделать случайное число между диапазоном для arc4random_uniform ()?
  • От String до NSDate в Swift
  • Как я могу использовать UserDefaults в Swift?
  • Ведущие нули для Int в Swift
  • Держите окно всегда сверху?
  • Давайте будем гением компьютера.