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.