NSRange для діапазону <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, щоб їх використовувати!
— пазл

Відповіді:


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

Швидкий 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, я підозрюю, що зворотним викликом делегата буде надано лише дійсні діапазони, засновані на символах unicode в рядку, і тому це безпечно використовувати , але я особисто не перевіряв цього.
— Олексій Прецлав

2
@Zaph, це правда, моя думка полягає в тому UITextFieldDelegate, що textField:shouldChangeCharactersInRange:replacementString:метод не забезпечить діапазон, який починається або закінчується всередині символу unicode, оскільки зворотний виклик заснований на користувачі, що вводить символи з клавіатури, і не можна вводити лише частину символу unicode смайлика. Зауважте, що якби делегат був записаний в 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>) та 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в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.

The 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. Але він вважається одним символом Unicode у рядку Swift. - let end = advance(start, range.length)з посилається на відповідь, в цьому випадку виходить з ладу повідомлення про помилку "фатальна помилка: не можна збільшувати endIndex".
— Мартін Р

8
@MattDiPasquale: Звичайно. Я мав намір відповісти на дослівне запитання "Як я можу перетворити NSRange в діапазон <String.Index> в Swift" безпечним для Unicode способом (сподіваючись, що хтось може вважати його корисним, що досі не так :(
— Мартін R

5
Дякую ще раз за те, що ви зробили для них роботу Apple. Без таких людей, як ви, і Stack Overflow, я не можу займатися розробкою iOS / OS X. Життя занадто коротке.
— Womble

1
@ jiminybob99: Клацніть команду, 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 .

Я не знаю, який діапазон ви намагаєтеся замінити, але давайте зробимо вигляд, що хочете замінити перші два символи.

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)

18

Ця відповідь Мартіна R здається правильною, оскільки вона пояснює Unicode.

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

Швидкий 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
    }
}

Швидкий 4

extension String {
    func rangeFromNSRange(nsRange : NSRange) -> Range<String.Index>? {
        return Range(nsRange, in: self)
    }
}

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 байт, а (ЛИЦЕ З СЛУЧАМИ РАДИ) - 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

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

Ось мої найкращі зусилля. Але це не може перевірити або виявити неправильний аргумент введення.

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, це лише детальна інформація про реалізацію, і я не міг знайти жодної згадки про будь-яку гарантію. Це означає, що деталі реалізації можуть бути змінені в будь-який час. Внутрішнє представництво Свіфта 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)

У 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.
— Danny Bravo

0

Офіційна документація бета-версії Swift 3.0 запропонувала своє стандартне рішення для цієї ситуації під заголовком String.UTF16Переглянути в розділі UTF16View Elements Match NSString Characters title


Буде корисно, якщо ви надасте посилання у своїй відповіді.
— m5khan

0

У прийнятій відповіді я вважаю необов'язковістю громіздкі. Це працює з Swift 3 і, здається, не має проблем з емоджи.

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)
    }
}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.