Comment fonctionne String.Index dans Swift

109

J'ai mis à jour certains de mes anciens codes et réponses avec Swift 3, mais quand je suis arrivé à Swift Strings and Indexing, il a été difficile de comprendre les choses.

Plus précisément, j'essayais ce qui suit:

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

où la deuxième ligne me donnait l'erreur suivante

'advancedBy' n'est pas disponible: pour avancer un index de n étapes, appelez 'index (_: offsetBy :)' sur l'instance CharacterView qui a produit l'index.

Je vois que cela Stringa les méthodes suivantes.

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)

Celles-ci me déroutaient vraiment au début, alors j'ai commencé à jouer avec elles jusqu'à ce que je les comprenne. J'ajoute une réponse ci-dessous pour montrer comment ils sont utilisés.

Suragch
la source

Réponses:

280

entrez la description de l'image ici

Tous les exemples suivants utilisent

var str = "Hello, playground"

startIndex et endIndex

  • startIndex est l'index du premier caractère
  • endIndexest l'index après le dernier caractère.

Exemple

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

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

Avec les gammes unilatérales de Swift 4 , la gamme peut être simplifiée à l'une des formes suivantes.

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

J'utiliserai le formulaire complet dans les exemples suivants pour plus de clarté, mais pour des raisons de lisibilité, vous voudrez probablement utiliser les plages unilatérales dans votre code.

after

Un péché: index(after: String.Index)

  • after fait référence à l'index du caractère directement après l'index donné.

Exemples

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

Un péché: index(before: String.Index)

  • before fait référence à l'index du caractère juste avant l'index donné.

Exemples

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

Un péché: index(String.Index, offsetBy: String.IndexDistance)

  • La offsetByvaleur peut être positive ou négative et commence à partir de l'index donné. Bien qu'il soit du type String.IndexDistance, vous pouvez lui donner un fichier Int.

Exemples

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

Un péché: index(String.Index, offsetBy: String.IndexDistance, limitedBy: String.Index)

  • Le limitedByest utile pour s'assurer que le décalage ne fait pas sortir l'index des limites. C'est un index englobant. Puisqu'il est possible que le décalage dépasse la limite, cette méthode retourne une option. Il retourne nilsi l'index est hors limites.

Exemple

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

Si le décalage avait été 77au lieu de 7, l' ifinstruction aurait été ignorée.

Pourquoi String.Index est-il nécessaire?

Il serait beaucoup plus facile d'utiliser un Intindex pour les chaînes. La raison pour laquelle vous devez créer un nouveau String.Indexpour chaque chaîne est que les caractères dans Swift ne sont pas tous de la même longueur sous le capot. Un seul caractère Swift peut être composé d'un, deux ou même plusieurs points de code Unicode. Ainsi, chaque chaîne unique doit calculer les index de ses caractères.

C'est peut-être pour cacher cette complexité derrière une extension d'index Int, mais je suis réticent à le faire. Il est bon de se rappeler ce qui se passe réellement.

Suragch
la source
18
Pourquoi serait startIndexautre chose que 0?
Robo Robok
15
@RoboRobok: Étant donné que Swift fonctionne avec des caractères Unicode, qui sont constitués de "grappes de graphèmes", Swift n'utilise pas d'entiers pour représenter les emplacements d'index. Disons que votre premier personnage est un é. Il est en fait composé du eplus une \u{301}représentation Unicode. Si vous utilisez un index de zéro, vous obtiendrez soit le caractère eaccentué ("grave"), et non le cluster entier qui compose le é. L'utilisation de startIndexgarantit que vous obtiendrez le cluster de graphèmes complet pour n'importe quel caractère.
leanne
2
Dans Swift 4.0, chaque caractère Unicode est compté par 1. Par exemple: "👩‍💻" .count // Maintenant: 1, Avant: 2
selva
3
Comment construire un à String.Indexpartir d'un entier, autre que de construire une chaîne factice et d'utiliser la .indexméthode dessus? Je ne sais pas si je manque quelque chose, mais la documentation ne dit rien.
sudo
3
@sudo, vous devez être un peu prudent lorsque vous construisez un String.Indexavec un entier car chaque Swift Charactern'équivaut pas nécessairement à la même chose que vous entendez avec un entier. Cela dit, vous pouvez passer un entier dans le offsetByparamètre pour créer un fichier String.Index. StringCependant, si vous n'avez pas de a , vous ne pouvez pas construire de String.Index(car Swift ne peut calculer l'index que s'il sait quels sont les caractères précédents de la chaîne). Si vous modifiez la chaîne, vous devez recalculer l'index. Vous ne pouvez pas utiliser la même chose String.Indexsur deux chaînes différentes.
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
la source
-2

Créez un UITextView à l'intérieur d'un tableViewController. J'ai utilisé la fonction: textViewDidChange, puis j'ai vérifié l'entrée clé de retour. puis s'il a détecté une entrée par touche de retour, supprimez l'entrée de la touche de retour et fermez le clavier.

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
la source