Como String.Index funciona em Swift

109

Estive atualizando alguns dos meus códigos antigos e respostas com o Swift 3, mas quando cheguei ao Swift Strings and Indexing, foi uma dor entender as coisas.

Especificamente, eu estava tentando o seguinte:

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

onde a segunda linha estava me dando o seguinte erro

'advancedBy' não está disponível: Para avançar um índice em n etapas, chame 'index (_: offsetBy :)' na instância CharacterView que produziu o índice.

Vejo que Stringtem os seguintes métodos.

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)

Isso realmente me confundiu no início, então comecei a brincar com eles até entendê-los. Estou adicionando uma resposta abaixo para mostrar como eles são usados.

Suragch
fonte

Respostas:

280

insira a descrição da imagem aqui

Todos os exemplos a seguir usam

var str = "Hello, playground"

startIndex e endIndex

  • startIndex é o índice do primeiro personagem
  • endIndexé o índice após o último caractere.

Exemplo

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

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

Com os intervalos unilaterais do Swift 4 , o intervalo pode ser simplificado para uma das seguintes formas.

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

Usarei a forma completa nos exemplos a seguir por uma questão de clareza, mas por uma questão de legibilidade, você provavelmente desejará usar os intervalos unilaterais em seu código.

after

Como em: index(after: String.Index)

  • after refere-se ao índice do personagem diretamente após o índice fornecido.

Exemplos

// 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

Como em: index(before: String.Index)

  • before refere-se ao índice do personagem diretamente antes do índice fornecido.

Exemplos

// 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

Como em: index(String.Index, offsetBy: String.IndexDistance)

  • O offsetByvalor pode ser positivo ou negativo e começa a partir do índice fornecido. Embora seja do tipo String.IndexDistance, você pode atribuir um Int.

Exemplos

// 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

Como em: index(String.Index, offsetBy: String.IndexDistance, limitedBy: String.Index)

  • O limitedByé útil para garantir que o deslocamento não faça com que o índice saia dos limites. É um índice limitante. Como é possível que o deslocamento exceda o limite, este método retorna um opcional. Ele retorna nilse o índice está fora dos limites.

Exemplo

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

Se o deslocamento tivesse sido em 77vez de 7, a ifinstrução teria sido ignorada.

Por que String.Index é necessário?

Seria muito mais fácil usar um Intíndice para Strings. O motivo pelo qual você deve criar um novo String.Indexpara cada String é que os caracteres em Swift não têm todos o mesmo comprimento sob o capô. Um único caractere Swift pode ser composto de um, dois ou até mais pontos de código Unicode. Assim, cada String única deve calcular os índices de seus Personagens.

É possível ocultar essa complexidade por trás de uma extensão de índice Int, mas estou relutante em fazer isso. É bom ser lembrado do que está realmente acontecendo.

Suragch
fonte
18
Por que startIndexseria qualquer outra coisa senão 0?
Robo Robok
15
@RoboRobok: Como o Swift trabalha com caracteres Unicode, que são feitos de "clusters de grafemas", o Swift não usa números inteiros para representar as localizações do índice. Digamos que seu primeiro personagem seja um é. Na verdade, é feito de emais uma \u{301}representação Unicode. Se você usasse um índice zero, obteria o eou o caractere de acento ("grave"), não o cluster inteiro que compõe o é. Usar o startIndexgarante que você obterá todo o agrupamento de grafemas para qualquer personagem.
leanne
2
No Swift 4.0, cada caractere Unicode é contado por 1. Ex: "👩‍💻" .count // Agora: 1, Antes: 2
selva
3
Como alguém constrói a a String.Indexpartir de um inteiro, diferente de construir uma string fictícia e usar o .indexmétodo nela? Não sei se estou faltando alguma coisa, mas os médicos não dizem nada.
sudo de
3
@sudo, você deve ter um pouco de cuidado ao construir um String.Indexcom um inteiro porque cada Swift Characternão é necessariamente igual à mesma coisa que você quer dizer com um inteiro. Dito isso, você pode passar um número inteiro no offsetByparâmetro para criar um String.Index. StringPorém, se você não tiver um , não poderá construir um String.Index(porque o Swift só pode calcular o índice se souber quais são os caracteres anteriores da string). Se você alterar a string, deverá recalcular o índice. Você não pode usar o mesmo String.Indexem duas cordas diferentes.
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)
    }

}
Pavel Zavarin
fonte
-2

Crie um UITextView dentro de um tableViewController. Usei function: textViewDidChange e verifiquei a entrada da tecla return. então, se detectou a entrada da tecla de retorno, exclua a entrada da tecla de retorno e descarte o teclado.

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()
}
Karl Mogensen
fonte