NSRange для диапазона <String.Index>
Как преобразовать NSRange
в Range<String.Index>
в Swift?
Я хочу использовать следующий метод UITextFieldDelegate
:
func textField(textField: UITextField!,
shouldChangeCharactersInRange range: NSRange,
replacementString string: String!) -> Bool {
textField.text.stringByReplacingCharactersInRange(???, withString: string)
![enter image description here]()
Ответы
Ответ 1
Версия 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
Как и у Swift 4 (Xcode 9), стандарт Swift
библиотека предоставляет методы для преобразования строк строки Swift
(Range<String.Index>
) и 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: 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 = "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)) // 🇩🇪
Ответ 3
Вам нужно использовать 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)
Ответ 4
Этот ответ от 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
}
}
Ответ 5
Это похоже на ответ Эмилии, однако, поскольку вы спросили, как преобразовать 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)
...
}
Ответ 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
Ответ 7
func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool {
let strString = ((textField.text)! as NSString).stringByReplacingCharactersInRange(range, withString: string)
}
Ответ 8
В 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)
Ответ 9
Вот мои лучшие усилия. Но это не может проверить или обнаружить неправильный входной аргумент.
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)
Ответ 10
Я нашел, что самое чистое решение 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
}
Ответ 11
Официальная документация по Swift 3.0 beta предоставила стандартное решение для этой ситуации под заголовком String.UTF16View в разделе UTF16View Элементы Match NSString Название персонажа
Ответ 12
В принятом ответе я нахожу, что варианты громоздки. Это работает с 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
}