Как String.Index работает в Swift

109

Я обновлял часть своего старого кода и ответов с помощью Swift 3, но когда я добрался до Swift Strings и Indexing, мне было трудно понять вещи.

В частности, я пробовал следующее:

let str = "Hello, playground"
let prefixRange = str.startIndex..<str.startIndex.advancedBy(5) // error

где вторая строка дала мне следующую ошибку

«advancedBy» недоступен: чтобы продвинуть индекс на n шагов, вызовите «index (_: offsetBy :)» в экземпляре CharacterView, создавшем индекс.

Я вижу, что Stringесть следующие методы.

str.index(after: String.Index)
str.index(before: String.Index)
str.index(String.Index, offsetBy: String.IndexDistance)
str.index(String.Index, offsetBy: String.IndexDistance, limitedBy: String.Index)

Сначала они действительно сбивали меня с толку, поэтому я начал экспериментировать с ними, пока не понял их. Я добавляю ответ ниже, чтобы показать, как они используются.

Suragch
источник

Ответы:

282

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

Во всех следующих примерах используется

var str = "Hello, playground"

startIndex и endIndex

  • startIndex это индекс первого символа
  • endIndexэто индекс после последнего символа.

пример

// character
str[str.startIndex] // H
str[str.endIndex]   // error: after last character

// range
let range = str.startIndex..<str.endIndex
str[range]  // "Hello, playground"

С помощью односторонних диапазонов Swift 4 диапазон можно упростить до одной из следующих форм.

let range = str.startIndex...
let range = ..<str.endIndex

Я буду использовать полную форму в следующих примерах для ясности, но для удобства чтения вы, вероятно, захотите использовать односторонние диапазоны в своем коде.

after

Как в: index(after: String.Index)

  • after относится к индексу символа сразу после данного индекса.

Примеры

// character
let index = str.index(after: str.startIndex)
str[index]  // "e"

// range
let range = str.index(after: str.startIndex)..<str.endIndex
str[range]  // "ello, playground"

before

Как в: index(before: String.Index)

  • before относится к индексу символа непосредственно перед данным индексом.

Примеры

// character
let index = str.index(before: str.endIndex)
str[index]  // d

// range
let range = str.startIndex..<str.index(before: str.endIndex)
str[range]  // Hello, playgroun

offsetBy

Как в: index(String.Index, offsetBy: String.IndexDistance)

  • offsetByЗначение может быть положительным или отрицательным , и начинается с заданного индекса. Хотя он и относится к типу String.IndexDistance, вы можете присвоить ему Int.

Примеры

// character
let index = str.index(str.startIndex, offsetBy: 7)
str[index]  // p

// range
let start = str.index(str.startIndex, offsetBy: 7)
let end = str.index(str.endIndex, offsetBy: -6)
let range = start..<end
str[range]  // play

limitedBy

Как в: index(String.Index, offsetBy: String.IndexDistance, limitedBy: String.Index)

  • Это limitedByполезно для того, чтобы убедиться, что смещение не приводит к выходу индекса за пределы. Это ограничивающий индекс. Поскольку смещение может превышать предел, этот метод возвращает значение Optional. Он возвращается, nilесли индекс выходит за пределы.

пример

// character
if let index = str.index(str.startIndex, offsetBy: 7, limitedBy: str.endIndex) {
    str[index]  // p
}

Если бы смещение было 77вместо 7, то ifоператор был бы пропущен.

Зачем нужен String.Index?

Было бы намного проще использовать Intиндекс для строк. Причина, по которой вам нужно создавать новую String.Indexстроку для каждой строки, заключается в том, что символы в Swift не имеют одинаковой длины под капотом. Один символ Swift может состоять из одной, двух или даже более кодовых точек Unicode. Таким образом, каждая уникальная строка должна вычислять индексы своих символов.

Возможно, эту сложность можно скрыть за расширением индекса Int, но я не хочу этого делать. Приятно напоминать о том, что происходит на самом деле.

Suragch
источник
18
Почему должно startIndexбыть что-то еще, кроме 0?
Robo Robok
15
@RoboRobok: поскольку Swift работает с символами Unicode, которые состоят из «кластеров графемы», Swift не использует целые числа для представления местоположений индексов. Допустим, ваш первый персонаж - это é. На самом деле он состоит из представления Юникода eплюс \u{301}. Если бы вы использовали нулевой индекс, вы бы получили либо знак eударения («серьезный»), а не весь кластер, составляющий é. Использование startIndexгарантирует, что вы получите весь кластер графем для любого персонажа.
Leanne
2
В Swift 4.0 каждый символ Unicode считается на 1. Например: "👩‍💻" .count // Сейчас: 1, До: 2
сельва
3
Как можно построить a String.Indexиз целого числа, кроме создания фиктивной строки и использования на ней .indexметода? Не знаю, упустил ли я что-то, но в документации ничего не сказано.
sudo
3
@sudo, вы должны быть немного осторожны при построении String.Indexс целым числом, потому что каждый Swift Characterне обязательно равен тому же самому, что вы имеете в виду с целым числом. Тем не менее, вы можете передать целое число в offsetByпараметр для создания файла String.Index. Однако, если у вас нет a String, вы не можете построить a String.Index(потому что Swift может вычислить индекс только в том случае, если он знает, какие предыдущие символы в строке). Если вы измените строку, вам придется пересчитать индекс. Вы не можете использовать одно и то же String.Indexна двух разных струнах.
Suragch
0
func change(string: inout String) {

    var character: Character = .normal

    enum Character {
        case space
        case newLine
        case normal
    }

    for i in stride(from: string.count - 1, through: 0, by: -1) {
        // first get index
        let index: String.Index?
        if i != 0 {
            index = string.index(after: string.index(string.startIndex, offsetBy: i - 1))
        } else {
            index = string.startIndex
        }

        if string[index!] == "\n" {

            if character != .normal {

                if character == .newLine {
                    string.remove(at: index!)
                } else if character == .space {
                    let number = string.index(after: string.index(string.startIndex, offsetBy: i))
                    if string[number] == " " {
                        string.remove(at: number)
                    }
                    character = .newLine
                }

            } else {
                character = .newLine
            }

        } else if string[index!] == " " {

            if character != .normal {

                string.remove(at: index!)

            } else {
                character = .space
            }

        } else {

            character = .normal

        }

    }

    // startIndex
    guard string.count > 0 else { return }
    if string[string.startIndex] == "\n" || string[string.startIndex] == " " {
        string.remove(at: string.startIndex)
    }

    // endIndex - here is a little more complicated!
    guard string.count > 0 else { return }
    let index = string.index(before: string.endIndex)
    if string[index] == "\n" || string[index] == " " {
        string.remove(at: index)
    }

}
Павел Заварин
источник
-2

Создайте UITextView внутри tableViewController. Я использовал функцию: textViewDidChange, а затем проверил ввод ключа возврата. затем, если он обнаружил ввод клавиши возврата, удалите ввод клавиши возврата и закройте клавиатуру.

func textViewDidChange(_ textView: UITextView) {
    tableView.beginUpdates()
    if textView.text.contains("\n"){
        textView.text.remove(at: textView.text.index(before: textView.text.endIndex))
        textView.resignFirstResponder()
    }
    tableView.endUpdates()
}
Карл Могенсен
источник