¿Cómo funciona String.Index en Swift?

108

He estado actualizando algunos de mis antiguos códigos y respuestas con Swift 3, pero cuando llegué a Swift Strings and Indexing, ha sido un fastidio entender las cosas.

Específicamente, estaba intentando lo siguiente:

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

donde la segunda línea me estaba dando el siguiente error

'advancedBy' no está disponible: para avanzar un índice n pasos, llame a 'index (_: offsetBy :)' en la instancia de CharacterView que produjo el índice.

Veo que Stringtiene los siguientes 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)

Estos me confundieron mucho al principio, así que comencé a jugar con ellos hasta que los entendí. Estoy agregando una respuesta a continuación para mostrar cómo se usan.

Suragch
fuente

Respuestas:

280

ingrese la descripción de la imagen aquí

Todos los siguientes ejemplos usan

var str = "Hello, playground"

startIndex y endIndex

  • startIndex es el índice del primer carácter
  • endIndexes el índice después del último carácter.

Ejemplo

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

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

Con los rangos unilaterales de Swift 4 , el rango se puede simplificar a una de las siguientes formas.

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

Usaré el formulario completo en los siguientes ejemplos en aras de la claridad, pero en aras de la legibilidad, probablemente querrá usar los rangos unilaterales en su código.

after

Como en: index(after: String.Index)

  • after se refiere al índice del carácter directamente después del índice dado.

Ejemplos

// 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 en: index(before: String.Index)

  • before se refiere al índice del carácter directamente antes del índice dado.

Ejemplos

// 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 en: index(String.Index, offsetBy: String.IndexDistance)

  • El offsetByvalor puede ser positivo o negativo y comienza desde el índice dado. Aunque es del tipo String.IndexDistance, puedes darle un Int.

Ejemplos

// 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 en: index(String.Index, offsetBy: String.IndexDistance, limitedBy: String.Index)

  • El limitedByes útil para asegurarse de que el desplazamiento no hace que el índice para ir fuera de límites. Es un índice delimitador. Dado que es posible que el desplazamiento exceda el límite, este método devuelve un Opcional. Vuelve nilsi el índice está fuera de los límites.

Ejemplo

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

Si el desplazamiento hubiera sido en 77lugar de 7, entonces la ifdeclaración se habría omitido.

¿Por qué se necesita String.Index?

Sería mucho más fácil usar un Intíndice para Strings. La razón por la que tienes que crear una nueva String.Indexpara cada cadena es que los personajes en Swift no tienen la misma longitud debajo del capó. Un solo carácter Swift puede estar compuesto por uno, dos o incluso más puntos de código Unicode. Por tanto, cada Cadena única debe calcular los índices de sus Caracteres.

Posiblemente sea para ocultar esta complejidad detrás de una extensión de índice Int, pero soy reacio a hacerlo. Es bueno recordar lo que realmente está sucediendo.

Suragch
fuente
18
¿Por qué sería startIndexalgo más que 0?
Robo Robok
15
@RoboRobok: debido a que Swift funciona con caracteres Unicode, que están hechos de "grupos de grafemas", Swift no usa números enteros para representar ubicaciones de índices. Digamos que tu primer personaje es un é. En realidad, está hecho de emás una \u{301}representación Unicode. Si usara un índice de cero, obtendría eel carácter de acento o ("grave"), no todo el grupo que forma el é. El uso de startIndexasegura que obtendrá todo el grupo de grafemas para cualquier personaje.
leanne
2
En Swift 4.0, cada carácter Unicode se cuenta por 1. Ej .: "👩‍💻" .count // Ahora: 1, Antes: 2
selva
3
¿Cómo se construye a a String.Indexpartir de un número entero, además de construir una cadena ficticia y usar el .indexmétodo en ella? No sé si me estoy perdiendo algo, pero los médicos no dicen nada.
sudo
3
@sudo, debes tener un poco de cuidado al construir un String.Indexcon un número entero porque cada Swift Characterno necesariamente equivale a lo mismo que quieres decir con un número entero. Dicho esto, puede pasar un número entero al offsetByparámetro para crear un String.Index. Sin embargo, si no tiene un String, no puede construir un String.Index(porque Swift solo puede calcular el índice si sabe cuáles son los caracteres anteriores en la cadena). Si cambia la cadena, debe volver a calcular el índice. No puede usar lo mismo String.Indexen dos cadenas 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
fuente
-2

Cree un UITextView dentro de un tableViewController. Usé la función: textViewDidChange y luego verifiqué return-key-input. luego, si detectó return-key-input, borre la entrada de la tecla return y descarte el 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
fuente